@shortkitsdk/react-native 0.2.6 → 0.2.12

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 (75) hide show
  1. package/ShortKitReactNative.podspec +1 -0
  2. package/android/build.gradle.kts +17 -1
  3. package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +379 -0
  4. package/android/src/main/java/com/shortkit/reactnative/ReactLoadingHost.kt +40 -0
  5. package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +570 -0
  6. package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +1029 -0
  7. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +212 -219
  8. package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +17 -3
  9. package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +157 -742
  10. package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +11 -2
  11. package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +2 -2
  12. package/ios/ReactCarouselOverlayHost.swift +177 -0
  13. package/ios/ReactLoadingHost.swift +38 -0
  14. package/ios/ReactOverlayHost.swift +444 -0
  15. package/ios/SKFabricSurfaceWrapper.h +18 -0
  16. package/ios/SKFabricSurfaceWrapper.mm +57 -0
  17. package/ios/ShortKitBridge.swift +220 -63
  18. package/ios/ShortKitFeedView.swift +82 -228
  19. package/ios/ShortKitFeedViewManager.mm +3 -2
  20. package/ios/ShortKitModule.mm +69 -37
  21. package/ios/ShortKitPlayerNativeView.swift +39 -8
  22. package/ios/ShortKitReactNative-Bridging-Header.h +2 -0
  23. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
  24. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +3683 -1249
  25. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +56 -15
  26. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  27. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +56 -15
  28. package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  29. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
  30. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +3683 -1249
  31. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +56 -15
  32. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  33. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +56 -15
  34. package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  35. package/ios/ShortKitSDK.xcframework.bak/Info.plist +43 -0
  36. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  37. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
  38. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +28917 -0
  39. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +824 -0
  40. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  41. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +824 -0
  42. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  43. package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
  44. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
  45. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
  46. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +28917 -0
  47. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +824 -0
  48. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  49. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +824 -0
  50. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
  51. package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
  52. package/ios/ShortKitWidgetNativeView.swift +3 -3
  53. package/package.json +1 -1
  54. package/src/ShortKitCarouselOverlaySurface.tsx +55 -0
  55. package/src/ShortKitCommands.ts +31 -0
  56. package/src/ShortKitContext.ts +6 -24
  57. package/src/ShortKitFeed.tsx +124 -41
  58. package/src/ShortKitLoadingSurface.tsx +24 -0
  59. package/src/ShortKitOverlaySurface.tsx +313 -0
  60. package/src/ShortKitPlayer.tsx +30 -9
  61. package/src/ShortKitProvider.tsx +28 -285
  62. package/src/index.ts +9 -3
  63. package/src/serialization.ts +20 -39
  64. package/src/specs/NativeShortKitModule.ts +74 -45
  65. package/src/specs/ShortKitFeedViewNativeComponent.ts +3 -2
  66. package/src/types.ts +84 -16
  67. package/src/useShortKit.ts +1 -3
  68. package/src/useShortKitPlayer.ts +7 -7
  69. package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +0 -48
  70. package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +0 -128
  71. package/ios/ShortKitCarouselOverlayBridge.swift +0 -219
  72. package/ios/ShortKitOverlayBridge.swift +0 -111
  73. package/src/CarouselOverlayManager.tsx +0 -70
  74. package/src/OverlayManager.tsx +0 -87
  75. package/src/useShortKitCarousel.ts +0 -29
@@ -1,881 +1,296 @@
1
1
  package com.shortkit.reactnative
2
2
 
3
+ import android.os.Handler
4
+ import android.os.Looper
3
5
  import com.facebook.react.bridge.Arguments
6
+ import com.facebook.react.bridge.Promise
4
7
  import com.facebook.react.bridge.ReactApplicationContext
5
8
  import com.facebook.react.bridge.ReactMethod
6
9
  import com.facebook.react.bridge.WritableMap
7
10
  import com.facebook.react.modules.core.DeviceEventManagerModule
8
- import com.shortkit.CarouselImage
9
- import com.shortkit.ContentItem
10
- import com.shortkit.ContentSignal
11
- import com.shortkit.FeedInput
12
- import com.shortkit.ImageCarouselItem
13
- import com.shortkit.FeedConfig
14
- import com.shortkit.FeedHeight
15
- import com.shortkit.FeedSource
16
- import com.shortkit.FeedTransitionPhase
17
- import com.shortkit.JsonValue
18
- import com.shortkit.ShortKit
19
- import com.shortkit.ShortKitPlayer
20
- import com.shortkit.VideoOverlayMode
21
- import com.shortkit.CarouselOverlayMode
22
- import com.shortkit.SurveyOverlayMode
23
- import com.shortkit.AdOverlayMode
24
- import kotlinx.coroutines.CoroutineScope
25
- import kotlinx.coroutines.Dispatchers
26
- import kotlinx.coroutines.SupervisorJob
27
- import kotlinx.coroutines.cancel
28
- import kotlinx.coroutines.launch
29
- import org.json.JSONArray
30
- import org.json.JSONObject
31
11
 
12
+ /**
13
+ * Thin TurboModule that delegates all logic to [ShortKitBridge].
14
+ *
15
+ * Responsibilities:
16
+ * - Forward every @ReactMethod to the bridge
17
+ * - Handle event emission via RCTDeviceEventEmitter
18
+ * - Buffer operations that arrive before initialize() creates the bridge
19
+ */
32
20
  class ShortKitModule(reactContext: ReactApplicationContext) :
33
21
  NativeShortKitModuleSpec(reactContext) {
34
22
 
35
23
  companion object {
36
24
  const val NAME = "ShortKitModule"
37
-
38
- /** Static reference for Fabric view access (mirrors iOS ShortKitBridge.shared). */
39
- @Volatile
40
- var shared: ShortKitModule? = null
41
- private set
42
25
  }
43
26
 
44
- // -----------------------------------------------------------------------
45
- // State
46
- // -----------------------------------------------------------------------
47
-
48
- private var shortKit: ShortKit? = null
49
- private var scope: CoroutineScope? = null
27
+ private var bridge: ShortKitBridge? = null
28
+ private var pendingBridgeOps: MutableList<() -> Unit>? = null
50
29
  private var listenerCount = 0
51
- @Volatile
52
- private var hasListeners = false
53
- private var pendingFeedItems: String? = null
54
- private var pendingAppendItems: String? = null
55
-
56
- /** Expose the underlying SDK for the Fabric feed view manager. */
57
- val sdk: ShortKit? get() = shortKit
30
+ @Volatile private var hasListeners = false
58
31
 
59
- // -----------------------------------------------------------------------
32
+ // -----------------------------------------------------------------
60
33
  // Module boilerplate
61
- // -----------------------------------------------------------------------
34
+ // -----------------------------------------------------------------
62
35
 
63
36
  override fun getName(): String = NAME
64
37
 
65
38
  override fun initialize() {
66
39
  super.initialize()
67
- shared = this
68
40
  }
69
41
 
70
42
  override fun onCatalystInstanceDestroy() {
71
- teardown()
72
- if (shared === this) shared = null
43
+ bridge?.teardown()
44
+ bridge = null
73
45
  super.onCatalystInstanceDestroy()
74
46
  }
75
47
 
76
- // -----------------------------------------------------------------------
77
- // Event listeners
78
- // -----------------------------------------------------------------------
48
+ // -----------------------------------------------------------------
49
+ // Event listener management
50
+ // -----------------------------------------------------------------
79
51
 
80
- override fun addListener(eventType: String?) {
52
+ @ReactMethod
53
+ fun addListener(eventType: String?) {
81
54
  listenerCount++
82
55
  hasListeners = true
83
56
  }
84
57
 
85
- override fun removeListeners(count: Double) {
58
+ @ReactMethod
59
+ fun removeListeners(count: Double) {
86
60
  listenerCount = maxOf(0, listenerCount - count.toInt())
87
61
  hasListeners = listenerCount > 0
88
62
  }
89
63
 
90
- // -----------------------------------------------------------------------
91
- // Lifecycle methods
92
- // -----------------------------------------------------------------------
64
+ // -----------------------------------------------------------------
65
+ // Lifecycle
66
+ // -----------------------------------------------------------------
93
67
 
94
68
  @ReactMethod
95
69
  override fun initialize(
96
70
  apiKey: String,
97
- config: String,
71
+ hasLoadingView: Boolean,
98
72
  clientAppName: String?,
99
73
  clientAppVersion: String?,
100
74
  customDimensions: String?
101
75
  ) {
102
- // Tear down any existing instance (re-init safety)
103
- teardown()
104
-
105
- val feedConfig = parseFeedConfig(config)
106
- val dims = parseCustomDimensions(customDimensions)
76
+ bridge?.teardown()
77
+ bridge = null
107
78
 
108
79
  val context = reactApplicationContext
109
80
 
110
- val sdk = ShortKit(
111
- context = context,
112
- apiKey = apiKey,
113
- config = feedConfig,
114
- userId = null,
115
- adProvider = null,
116
- clientAppName = clientAppName,
117
- clientAppVersion = clientAppVersion,
118
- customDimensions = dims
119
- )
120
- this.shortKit = sdk
121
- shared = this
122
-
123
- // Replay any feed items that arrived before initialization
124
- pendingFeedItems?.let { json ->
125
- parseFeedInputs(json)?.let { sdk.setFeedItems(it) }
126
- pendingFeedItems = null
127
- }
128
- pendingAppendItems?.let { json ->
129
- parseFeedInputs(json)?.let { sdk.appendFeedItems(it) }
130
- pendingAppendItems = null
131
- }
132
-
133
- subscribeToFlows(sdk.player)
81
+ Handler(Looper.getMainLooper()).post {
82
+ bridge = ShortKitBridge(
83
+ apiKey = apiKey,
84
+ context = context,
85
+ hasLoadingView = hasLoadingView,
86
+ clientAppName = clientAppName,
87
+ clientAppVersion = clientAppVersion,
88
+ customDimensionsJSON = customDimensions,
89
+ emitEvent = { name, body -> sendEvent(name, body) }
90
+ )
134
91
 
135
- sdk.delegate = object : com.shortkit.ShortKitDelegate {
136
- override fun onContentTapped(contentId: String, index: Int) {
137
- val params = Arguments.createMap().apply {
138
- putString("contentId", contentId)
139
- putInt("index", index)
140
- }
141
- sendEvent("onContentTapped", params)
142
- }
92
+ pendingBridgeOps?.forEach { it() }
93
+ pendingBridgeOps = null
143
94
  }
144
95
  }
145
96
 
97
+ @ReactMethod
98
+ override fun destroy() {
99
+ bridge?.teardown()
100
+ bridge = null
101
+ }
102
+
146
103
  @ReactMethod
147
104
  override fun setUserId(userId: String) {
148
- shortKit?.setUserId(userId)
105
+ bridge?.setUserId(userId)
149
106
  }
150
107
 
151
108
  @ReactMethod
152
109
  override fun clearUserId() {
153
- shortKit?.clearUserId()
110
+ bridge?.clearUserId()
154
111
  }
155
112
 
156
113
  @ReactMethod
157
114
  override fun onPause() {
158
- shortKit?.pause()
115
+ bridge?.onPause()
159
116
  }
160
117
 
161
- /// Called when the app foregrounds. We do NOT auto-resume here because:
162
- /// 1. The user may have manually paused before backgrounding.
163
- /// 2. The ShortKit SDK's internal lifecycle management already resumes
164
- /// playback when appropriate via Activity lifecycle callbacks.
165
118
  @ReactMethod
166
119
  override fun onResume() {
167
- // No-op: let the SDK's internal lifecycle handle resume
120
+ bridge?.onResume()
168
121
  }
169
122
 
170
- @ReactMethod
171
- override fun destroy() {
172
- teardown()
173
- }
174
-
175
- // -----------------------------------------------------------------------
123
+ // -----------------------------------------------------------------
176
124
  // Player controls
177
- // -----------------------------------------------------------------------
125
+ // -----------------------------------------------------------------
178
126
 
179
127
  @ReactMethod
180
- override fun play() {
181
- shortKit?.player?.play()
182
- }
128
+ override fun play() { bridge?.play() }
183
129
 
184
130
  @ReactMethod
185
- override fun pause() {
186
- shortKit?.player?.pause()
187
- }
131
+ override fun pause() { bridge?.pause() }
188
132
 
189
133
  @ReactMethod
190
- override fun seek(seconds: Double) {
191
- shortKit?.player?.seek(seconds)
192
- }
134
+ override fun seek(seconds: Double) { bridge?.seek(seconds) }
193
135
 
194
136
  @ReactMethod
195
- override fun seekAndPlay(seconds: Double) {
196
- shortKit?.player?.seekAndPlay(seconds)
197
- }
137
+ override fun seekAndPlay(seconds: Double) { bridge?.seekAndPlay(seconds) }
198
138
 
199
139
  @ReactMethod
200
- override fun skipToNext() {
201
- shortKit?.player?.skipToNext()
202
- }
140
+ override fun skipToNext() { bridge?.skipToNext() }
203
141
 
204
142
  @ReactMethod
205
- override fun skipToPrevious() {
206
- shortKit?.player?.skipToPrevious()
207
- }
143
+ override fun skipToPrevious() { bridge?.skipToPrevious() }
208
144
 
209
145
  @ReactMethod
210
- override fun setMuted(muted: Boolean) {
211
- shortKit?.player?.setMuted(muted)
212
- }
146
+ override fun setMuted(muted: Boolean) { bridge?.setMuted(muted) }
213
147
 
214
148
  @ReactMethod
215
- override fun setPlaybackRate(rate: Double) {
216
- shortKit?.player?.setPlaybackRate(rate.toFloat())
217
- }
149
+ override fun setPlaybackRate(rate: Double) { bridge?.setPlaybackRate(rate) }
218
150
 
219
151
  @ReactMethod
220
- override fun setCaptionsEnabled(enabled: Boolean) {
221
- shortKit?.player?.setCaptionsEnabled(enabled)
222
- }
152
+ override fun setCaptionsEnabled(enabled: Boolean) { bridge?.setCaptionsEnabled(enabled) }
223
153
 
224
154
  @ReactMethod
225
- override fun selectCaptionTrack(language: String) {
226
- shortKit?.player?.selectCaptionTrack(language)
227
- }
155
+ override fun selectCaptionTrack(language: String) { bridge?.selectCaptionTrack(language) }
228
156
 
229
157
  @ReactMethod
230
- override fun sendContentSignal(signal: String) {
231
- val contentSignal = if (signal == "positive") ContentSignal.POSITIVE else ContentSignal.NEGATIVE
232
- shortKit?.player?.sendContentSignal(contentSignal)
233
- }
158
+ override fun sendContentSignal(signal: String) { bridge?.sendContentSignal(signal) }
234
159
 
235
160
  @ReactMethod
236
- override fun setMaxBitrate(bitrate: Double) {
237
- shortKit?.player?.setMaxBitrate(bitrate.toInt())
238
- }
161
+ override fun setMaxBitrate(bitrate: Double) { bridge?.setMaxBitrate(bitrate) }
239
162
 
240
- // -----------------------------------------------------------------------
163
+ // -----------------------------------------------------------------
241
164
  // Custom feed
242
- // -----------------------------------------------------------------------
165
+ // -----------------------------------------------------------------
243
166
 
244
167
  @ReactMethod
245
- override fun setFeedItems(items: String) {
246
- val sdk = shortKit
247
- if (sdk != null) {
248
- val parsed = parseFeedInputs(items) ?: return
249
- sdk.setFeedItems(parsed)
168
+ override fun setFeedItems(feedId: String, items: String) {
169
+ val b = bridge
170
+ if (b != null) {
171
+ b.setFeedItems(feedId, items)
250
172
  } else {
251
- pendingFeedItems = items
173
+ bufferOp { bridge?.setFeedItems(feedId, items) }
252
174
  }
253
175
  }
254
176
 
255
177
  @ReactMethod
256
- override fun appendFeedItems(items: String) {
257
- val sdk = shortKit
258
- if (sdk != null) {
259
- val parsed = parseFeedInputs(items) ?: return
260
- sdk.appendFeedItems(parsed)
178
+ override fun appendFeedItems(feedId: String, items: String) {
179
+ val b = bridge
180
+ if (b != null) {
181
+ b.appendFeedItems(feedId, items)
261
182
  } else {
262
- pendingAppendItems = items
183
+ bufferOp { bridge?.appendFeedItems(feedId, items) }
263
184
  }
264
185
  }
265
186
 
266
187
  @ReactMethod
267
- override fun fetchContent(limit: Double, promise: com.facebook.react.bridge.Promise) {
268
- val sdk = shortKit
269
- if (sdk == null) {
188
+ override fun fetchContent(limit: Double, filterJSON: String?, promise: Promise) {
189
+ val b = bridge
190
+ if (b == null) {
270
191
  promise.resolve("[]")
271
192
  return
272
193
  }
273
- scope?.launch {
274
- try {
275
- val items = sdk.fetchContent(limit.toInt())
276
- val arr = JSONArray()
277
- for (item in items) {
278
- arr.put(JSONObject(serializeContentItemToJSON(item)))
279
- }
280
- promise.resolve(arr.toString())
281
- } catch (e: Exception) {
282
- promise.resolve("[]")
283
- }
284
- }
194
+ b.fetchContent(limit.toInt(), filterJSON) { result -> promise.resolve(result) }
285
195
  }
286
196
 
287
- // -----------------------------------------------------------------------
288
- // Storyboard / Seek Thumbnails
289
- // -----------------------------------------------------------------------
290
-
291
197
  @ReactMethod
292
- override fun prefetchStoryboard(playbackId: String) {
293
- shortKit?.player?.prefetchStoryboard(playbackId)
198
+ override fun applyFilter(feedId: String, filterJSON: String?) {
199
+ val b = bridge
200
+ if (b != null) {
201
+ b.applyFilter(feedId, filterJSON)
202
+ } else {
203
+ bufferOp { bridge?.applyFilter(feedId, filterJSON) }
204
+ }
294
205
  }
295
206
 
296
207
  @ReactMethod
297
- override fun getStoryboardData(playbackId: String, promise: com.facebook.react.bridge.Promise) {
298
- val player = shortKit?.player
299
- if (player == null) {
300
- promise.resolve(null)
208
+ override fun preloadFeed(configJSON: String, itemsJSON: String?, promise: Promise) {
209
+ val b = bridge
210
+ if (b == null) {
211
+ promise.resolve("")
301
212
  return
302
213
  }
303
- // Try cached data first
304
- val json = player.getStoryboardData(playbackId)
305
- if (json != null) {
306
- promise.resolve(json)
307
- return
308
- }
309
- // Trigger prefetch and retry after a short delay
310
- player.prefetchStoryboard(playbackId)
311
- scope?.launch {
312
- // Wait for prefetch to complete (poll with timeout)
313
- var retries = 0
314
- while (retries < 30) { // 3 seconds max
315
- kotlinx.coroutines.delay(100)
316
- val data = player.getStoryboardData(playbackId)
317
- if (data != null) {
318
- promise.resolve(data)
319
- return@launch
320
- }
321
- retries++
322
- }
323
- promise.resolve(null)
324
- }
214
+ b.preloadFeed(configJSON, itemsJSON) { result -> promise.resolve(result) }
325
215
  }
326
216
 
327
- // -----------------------------------------------------------------------
328
- // Overlay lifecycle events (called by Fabric view)
329
- // -----------------------------------------------------------------------
330
-
331
- fun emitOverlayEvent(name: String, item: ContentItem) {
332
- val params = Arguments.createMap().apply {
333
- putString("item", serializeContentItemToJSON(item))
334
- }
335
- sendEvent(name, params)
336
- }
337
-
338
- fun emitOverlayEvent(name: String, params: WritableMap) {
339
- sendEvent(name, params)
340
- }
341
-
342
- // -----------------------------------------------------------------------
343
- // Carousel overlay lifecycle events
344
- // -----------------------------------------------------------------------
217
+ // -----------------------------------------------------------------
218
+ // Storyboard / Seek Thumbnails
219
+ // -----------------------------------------------------------------
345
220
 
346
- fun emitCarouselOverlayEvent(name: String, params: WritableMap) {
347
- sendEvent(name, params)
221
+ @ReactMethod
222
+ override fun prefetchStoryboard(playbackId: String) {
223
+ bridge?.prefetchStoryboard(playbackId)
348
224
  }
349
225
 
350
- // -----------------------------------------------------------------------
351
- // Flow subscriptions
352
- // -----------------------------------------------------------------------
353
-
354
- private fun subscribeToFlows(player: ShortKitPlayer) {
355
- scope?.cancel()
356
- val newScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
357
- scope = newScope
358
-
359
- // Player state
360
- newScope.launch {
361
- player.playerState.collect { state ->
362
- val params = Arguments.createMap().apply {
363
- putString("state", playerStateString(state))
364
- if (state is com.shortkit.PlayerState.Error) {
365
- putString("errorMessage", state.message)
366
- }
367
- }
368
- sendEvent("onPlayerStateChanged", params)
369
- }
370
- }
371
-
372
- // Current item
373
- newScope.launch {
374
- player.currentItem.collect { item ->
375
- if (item != null) {
376
- sendEvent("onCurrentItemChanged", contentItemMap(item))
377
- }
378
- }
379
- }
380
-
381
- // Time updates
382
- newScope.launch {
383
- player.time.collect { time ->
384
- val params = Arguments.createMap().apply {
385
- putDouble("current", time.currentMs / 1000.0)
386
- putDouble("duration", time.durationMs / 1000.0)
387
- putDouble("buffered", time.bufferedMs / 1000.0)
388
- }
389
- sendEvent("onTimeUpdate", params)
390
- }
391
- }
392
-
393
- // Muted state
394
- newScope.launch {
395
- player.isMuted.collect { muted ->
396
- val params = Arguments.createMap().apply {
397
- putBoolean("isMuted", muted)
398
- }
399
- sendEvent("onMutedChanged", params)
400
- }
401
- }
402
-
403
- // Playback rate
404
- newScope.launch {
405
- player.playbackRate.collect { rate ->
406
- val params = Arguments.createMap().apply {
407
- putDouble("rate", rate.toDouble())
408
- }
409
- sendEvent("onPlaybackRateChanged", params)
410
- }
411
- }
412
-
413
- // Captions enabled
414
- newScope.launch {
415
- player.captionsEnabled.collect { enabled ->
416
- val params = Arguments.createMap().apply {
417
- putBoolean("enabled", enabled)
418
- }
419
- sendEvent("onCaptionsEnabledChanged", params)
420
- }
421
- }
422
-
423
- // Active caption track
424
- newScope.launch {
425
- player.activeCaptionTrack.collect { track ->
426
- if (track != null) {
427
- val params = Arguments.createMap().apply {
428
- putString("language", track.language)
429
- putString("label", track.label)
430
- putString("sourceUrl", track.url ?: "")
431
- }
432
- sendEvent("onActiveCaptionTrackChanged", params)
433
- }
434
- }
435
- }
436
-
437
- // Active cue (ms -> seconds)
438
- newScope.launch {
439
- player.activeCue.collect { cue ->
440
- if (cue != null) {
441
- val params = Arguments.createMap().apply {
442
- putString("text", cue.text)
443
- putDouble("startTime", cue.startMs / 1000.0)
444
- putDouble("endTime", cue.endMs / 1000.0)
445
- }
446
- sendEvent("onActiveCueChanged", params)
447
- }
448
- }
449
- }
450
-
451
- // Did loop
452
- newScope.launch {
453
- player.didLoop.collect { event ->
454
- val params = Arguments.createMap().apply {
455
- putString("contentId", event.contentId)
456
- putInt("loopCount", event.loopCount)
457
- }
458
- sendEvent("onDidLoop", params)
459
- }
460
- }
461
-
462
- // Feed transition
463
- newScope.launch {
464
- player.feedTransition.collect { event ->
465
- val params = Arguments.createMap().apply {
466
- putString("phase", when (event.phase) {
467
- FeedTransitionPhase.BEGAN -> "began"
468
- FeedTransitionPhase.ENDED -> "ended"
469
- })
470
- putString("direction", when (event.direction) {
471
- com.shortkit.FeedDirection.FORWARD -> "forward"
472
- com.shortkit.FeedDirection.BACKWARD -> "backward"
473
- else -> "forward"
474
- })
475
- if (event.from != null) {
476
- putString("fromItem", serializeContentItemToJSON(event.from!!))
477
- }
478
- if (event.to != null) {
479
- putString("toItem", serializeContentItemToJSON(event.to!!))
480
- }
481
- }
482
- sendEvent("onFeedTransition", params)
483
- }
484
- }
485
-
486
- // Format change (Long -> Double for bitrate)
487
- newScope.launch {
488
- player.formatChange.collect { event ->
489
- val params = Arguments.createMap().apply {
490
- putString("contentId", event.contentId)
491
- putDouble("fromBitrate", event.fromBitrate.toDouble())
492
- putDouble("toBitrate", event.toBitrate.toDouble())
493
- putString("fromResolution", event.fromResolution ?: "")
494
- putString("toResolution", event.toResolution ?: "")
495
- }
496
- sendEvent("onFormatChange", params)
497
- }
498
- }
499
-
500
- // Prefetched ahead count
501
- newScope.launch {
502
- player.prefetchedAheadCount.collect { count ->
503
- val params = Arguments.createMap().apply {
504
- putInt("count", count)
505
- }
506
- sendEvent("onPrefetchedAheadCountChanged", params)
507
- }
508
- }
509
-
510
- // Remaining content count
511
- newScope.launch {
512
- player.remainingContentCount.collect { count ->
513
- val params = Arguments.createMap().apply {
514
- putInt("count", count)
515
- }
516
- sendEvent("onRemainingContentCountChanged", params)
517
- }
226
+ @ReactMethod
227
+ override fun getStoryboardData(playbackId: String, promise: Promise) {
228
+ val b = bridge
229
+ if (b == null) {
230
+ promise.resolve(null)
231
+ return
518
232
  }
233
+ b.getStoryboardData(playbackId) { result -> promise.resolve(result) }
519
234
  }
520
235
 
521
- // -----------------------------------------------------------------------
236
+ // -----------------------------------------------------------------
522
237
  // Event emission
523
- // -----------------------------------------------------------------------
238
+ // -----------------------------------------------------------------
524
239
 
525
240
  private fun sendEvent(name: String, params: WritableMap) {
526
- if (!hasListeners) return
241
+ // Use the codegen-generated emitOn* methods which route through
242
+ // mEventEmitterCallback (JSI direct channel). The legacy
243
+ // DeviceEventManagerModule.RCTDeviceEventEmitter path does NOT
244
+ // reach codegen EventEmitter<T> subscribers in RN 0.78 new arch.
527
245
  try {
528
- reactApplicationContext
529
- .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
530
- .emit(name, params)
531
- } catch (_: Exception) {
532
- // Context may not have an active catalyst instance during teardown
533
- }
534
- }
535
-
536
- // -----------------------------------------------------------------------
537
- // Teardown
538
- // -----------------------------------------------------------------------
539
-
540
- private fun teardown() {
541
- scope?.cancel()
542
- scope = null
543
- shortKit?.release()
544
- shortKit = null
545
- if (shared === this) shared = null
546
- }
547
-
548
- // -----------------------------------------------------------------------
549
- // Content item serialization
550
- // -----------------------------------------------------------------------
551
-
552
- /**
553
- * Build a flat WritableMap for `onCurrentItemChanged`.
554
- * `captionTracks` and `customMetadata` are JSON-serialized strings.
555
- */
556
- private fun contentItemMap(item: ContentItem): WritableMap {
557
- return Arguments.createMap().apply {
558
- putString("id", item.id)
559
- item.playbackId?.let { putString("playbackId", it) }
560
- putString("title", item.title)
561
- item.description?.let { putString("description", it) }
562
- putDouble("duration", item.duration)
563
- putString("streamingUrl", item.streamingUrl)
564
- putString("thumbnailUrl", item.thumbnailUrl)
565
-
566
- // Caption tracks as JSON string
567
- putString("captionTracks", serializeCaptionTracks(item.captionTracks))
568
-
569
- // Custom metadata as JSON string
570
- item.customMetadata?.let { meta ->
571
- putString("customMetadata", serializeCustomMetadata(meta))
572
- }
573
-
574
- item.author?.let { putString("author", it) }
575
- item.articleUrl?.let { putString("articleUrl", it) }
576
- item.commentCount?.let { putInt("commentCount", it) }
577
- }
578
- }
579
-
580
- /**
581
- * Serialize a full ContentItem to a JSON string for delegate/overlay events.
582
- */
583
- private fun serializeContentItemToJSON(item: ContentItem): String {
584
- return try {
585
- val obj = JSONObject().apply {
586
- put("id", item.id)
587
- put("title", item.title)
588
- item.description?.let { put("description", it) }
589
- put("duration", item.duration)
590
- put("streamingUrl", item.streamingUrl)
591
- put("thumbnailUrl", item.thumbnailUrl)
592
- put("captionTracks", buildCaptionTracksJSONArray(item.captionTracks))
593
- item.customMetadata?.let { put("customMetadata", buildCustomMetadataJSONObject(it)) }
594
- item.author?.let { put("author", it) }
595
- item.articleUrl?.let { put("articleUrl", it) }
596
- item.commentCount?.let { put("commentCount", it) }
597
- }
598
- obj.toString()
599
- } catch (_: Exception) {
600
- "{}"
601
- }
602
- }
603
-
604
- /**
605
- * Serialize caption tracks list to a JSON string: `[{"language":"en","label":"English","sourceUrl":"..."}]`
606
- */
607
- private fun serializeCaptionTracks(tracks: List<com.shortkit.CaptionTrack>): String {
608
- return try {
609
- buildCaptionTracksJSONArray(tracks).toString()
610
- } catch (_: Exception) {
611
- "[]"
612
- }
613
- }
614
-
615
- private fun buildCaptionTracksJSONArray(tracks: List<com.shortkit.CaptionTrack>): JSONArray {
616
- val arr = JSONArray()
617
- for (track in tracks) {
618
- val obj = JSONObject().apply {
619
- put("language", track.language)
620
- put("label", track.label)
621
- put("sourceUrl", track.url ?: "")
622
- }
623
- arr.put(obj)
624
- }
625
- return arr
626
- }
627
-
628
- /**
629
- * Serialize custom metadata map to a JSON string.
630
- */
631
- private fun serializeCustomMetadata(meta: Map<String, JsonValue>): String {
632
- return try {
633
- buildCustomMetadataJSONObject(meta).toString()
634
- } catch (_: Exception) {
635
- "{}"
636
- }
637
- }
638
-
639
- private fun buildCustomMetadataJSONObject(meta: Map<String, JsonValue>): JSONObject {
640
- val obj = JSONObject()
641
- for ((key, value) in meta) {
642
- obj.put(key, jsonValueToAny(value))
643
- }
644
- return obj
645
- }
646
-
647
- /**
648
- * Convert a ShortKit JsonValue sealed class to a native type suitable for JSONObject.
649
- */
650
- private fun jsonValueToAny(value: JsonValue): Any {
651
- return when (value) {
652
- is JsonValue.StringValue -> value.value
653
- is JsonValue.NumberValue -> value.value
654
- is JsonValue.BoolValue -> value.value
655
- is JsonValue.ObjectValue -> {
656
- val obj = JSONObject()
657
- for ((k, v) in value.value) {
658
- obj.put(k, jsonValueToAny(v))
659
- }
660
- obj
661
- }
662
- is JsonValue.NullValue -> JSONObject.NULL
663
- }
664
- }
665
-
666
- // -----------------------------------------------------------------------
667
- // Player state serialization
668
- // -----------------------------------------------------------------------
669
-
670
- private fun playerStateString(state: com.shortkit.PlayerState): String {
671
- return when (state) {
672
- is com.shortkit.PlayerState.Idle -> "idle"
673
- is com.shortkit.PlayerState.Loading -> "loading"
674
- is com.shortkit.PlayerState.Ready -> "ready"
675
- is com.shortkit.PlayerState.Playing -> "playing"
676
- is com.shortkit.PlayerState.Paused -> "paused"
677
- is com.shortkit.PlayerState.Seeking -> "seeking"
678
- is com.shortkit.PlayerState.Buffering -> "buffering"
679
- is com.shortkit.PlayerState.Ended -> "ended"
680
- is com.shortkit.PlayerState.Error -> "error"
681
- }
682
- }
683
-
684
- // -----------------------------------------------------------------------
685
- // Config parsing
686
- // -----------------------------------------------------------------------
687
-
688
- /**
689
- * Parse the JSON config string from JS into a FeedConfig.
690
- *
691
- * Expected JSON shape:
692
- * ```json
693
- * {
694
- * "feedHeight": "{\"type\":\"fullscreen\"}",
695
- * "overlay": "\"none\"",
696
- * "carouselMode": "\"none\"",
697
- * "surveyMode": "\"none\"",
698
- * "muteOnStart": true
699
- * }
700
- * ```
701
- */
702
- private fun parseFeedConfig(json: String): FeedConfig {
703
- return try {
704
- val obj = JSONObject(json)
705
-
706
- val feedHeight = parseFeedHeight(obj.optString("feedHeight", null))
707
- val muteOnStart = obj.optBoolean("muteOnStart", true)
708
- val videoOverlay = parseVideoOverlay(obj.optString("overlay", null))
709
-
710
- val feedSourceStr = obj.optString("feedSource", "algorithmic")
711
- val feedSource = if (feedSourceStr == "custom") FeedSource.CUSTOM else FeedSource.ALGORITHMIC
712
-
713
- val carouselOverlay = parseCarouselOverlay(obj.optString("carouselOverlay", null))
714
-
715
- FeedConfig(
716
- feedHeight = feedHeight,
717
- videoOverlay = videoOverlay,
718
- carouselOverlay = carouselOverlay,
719
- surveyOverlay = SurveyOverlayMode.None,
720
- adOverlay = AdOverlayMode.None,
721
- muteOnStart = muteOnStart,
722
- feedSource = feedSource
723
- )
724
- } catch (_: Exception) {
725
- FeedConfig()
726
- }
727
- }
728
-
729
- /**
730
- * Parse a double-stringified feedHeight JSON.
731
- * e.g. `"{\"type\":\"fullscreen\"}"` or `"{\"type\":\"percentage\",\"value\":0.8}"`
732
- */
733
- private fun parseFeedHeight(json: String?): FeedHeight {
734
- if (json.isNullOrEmpty()) return FeedHeight.Fullscreen
735
- return try {
736
- val obj = JSONObject(json)
737
- when (obj.optString("type")) {
738
- "percentage" -> {
739
- val value = obj.optDouble("value", 1.0)
740
- FeedHeight.Percentage(value.toFloat())
246
+ when (name) {
247
+ "onPlayerStateChanged" -> emitOnPlayerStateChanged(params)
248
+ "onCurrentItemChanged" -> emitOnCurrentItemChanged(params)
249
+ "onTimeUpdate" -> emitOnTimeUpdate(params)
250
+ "onMutedChanged" -> emitOnMutedChanged(params)
251
+ "onPlaybackRateChanged" -> emitOnPlaybackRateChanged(params)
252
+ "onCaptionsEnabledChanged" -> emitOnCaptionsEnabledChanged(params)
253
+ "onActiveCaptionTrackChanged" -> emitOnActiveCaptionTrackChanged(params)
254
+ "onActiveCueChanged" -> emitOnActiveCueChanged(params)
255
+ "onDidLoop" -> emitOnDidLoop(params)
256
+ "onFeedTransition" -> emitOnFeedTransition(params)
257
+ "onFeedScrollPhase" -> emitOnFeedScrollPhase(params)
258
+ "onFormatChange" -> emitOnFormatChange(params)
259
+ "onPrefetchedAheadCountChanged" -> emitOnPrefetchedAheadCountChanged(params)
260
+ "onRemainingContentCountChanged" -> emitOnRemainingContentCountChanged(params)
261
+ "onSurveyResponse" -> emitOnSurveyResponse(params)
262
+ "onContentTapped" -> emitOnContentTapped(params)
263
+ "onDismiss" -> emitOnDismiss(params)
264
+ "onRefreshRequested" -> emitOnRefreshRequested(params)
265
+ "onDidFetchContentItems" -> emitOnDidFetchContentItems(params)
266
+ "onFeedReady" -> emitOnFeedReady(params)
267
+ "onOverlayActiveChanged" -> emitOnOverlayActiveChanged(params)
268
+ "onOverlayPlayerStateChanged" -> emitOnOverlayPlayerStateChanged(params)
269
+ "onOverlayMutedChanged" -> emitOnOverlayMutedChanged(params)
270
+ "onOverlayPlaybackRateChanged" -> emitOnOverlayPlaybackRateChanged(params)
271
+ "onOverlayCaptionsEnabledChanged" -> emitOnOverlayCaptionsEnabledChanged(params)
272
+ "onOverlayActiveCueChanged" -> emitOnOverlayActiveCueChanged(params)
273
+ "onOverlayFeedScrollPhaseChanged" -> emitOnOverlayFeedScrollPhaseChanged(params)
274
+ "onOverlayTimeUpdate" -> emitOnOverlayTimeUpdate(params)
275
+ "onOverlayFullState" -> emitOnOverlayFullState(params)
276
+ else -> {
277
+ android.util.Log.w("SK:Module", "sendEvent: unknown event name '$name', using legacy emitter")
278
+ reactApplicationContext
279
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
280
+ .emit(name, params)
741
281
  }
742
- else -> FeedHeight.Fullscreen
743
282
  }
744
- } catch (_: Exception) {
745
- FeedHeight.Fullscreen
283
+ } catch (e: Exception) {
284
+ android.util.Log.e("SK:Module", "sendEvent($name) EXCEPTION: ${e.message}", e)
746
285
  }
747
286
  }
748
287
 
749
- /**
750
- * Parse a double-stringified overlay JSON.
751
- * - `"\"none\""` -> None
752
- * - `"{\"type\":\"custom\"}"` -> Custom with bridge overlay factory
753
- */
754
- private fun parseVideoOverlay(json: String?): VideoOverlayMode {
755
- if (json.isNullOrEmpty()) return VideoOverlayMode.None
756
- return try {
757
- // Try parsing — might be a simple string "none" or an object
758
- val parsed = json.trim()
759
-
760
- // Strip outer quotes if double-stringified simple string
761
- if (parsed == "\"none\"" || parsed == "none") {
762
- return VideoOverlayMode.None
763
- }
288
+ // -----------------------------------------------------------------
289
+ // Op buffering
290
+ // -----------------------------------------------------------------
764
291
 
765
- // Try as JSON object
766
- val inner = if (parsed.startsWith("\"") && parsed.endsWith("\"")) {
767
- // Double-stringified: strip outer quotes and unescape
768
- JSONObject(parsed.substring(1, parsed.length - 1).replace("\\\"", "\""))
769
- } else {
770
- JSONObject(parsed)
771
- }
772
-
773
- if (inner.optString("type") == "custom") {
774
- // The Fabric view will handle the actual overlay view creation.
775
- // For the module, we signal custom mode so the SDK allocates an overlay slot.
776
- VideoOverlayMode.Custom { ShortKitOverlayBridge(reactApplicationContext) }
777
- } else {
778
- VideoOverlayMode.None
779
- }
780
- } catch (_: Exception) {
781
- VideoOverlayMode.None
782
- }
783
- }
784
-
785
- /**
786
- * Parse a double-stringified carousel overlay JSON.
787
- * - `"\"none\""` -> None
788
- * - `"{\"type\":\"custom\"}"` -> Custom with bridge overlay factory
789
- */
790
- private fun parseCarouselOverlay(json: String?): CarouselOverlayMode {
791
- if (json.isNullOrEmpty()) return CarouselOverlayMode.None
792
- return try {
793
- val parsed = json.trim()
794
-
795
- if (parsed == "\"none\"" || parsed == "none") {
796
- return CarouselOverlayMode.None
797
- }
798
-
799
- val inner = if (parsed.startsWith("\"") && parsed.endsWith("\"")) {
800
- JSONObject(parsed.substring(1, parsed.length - 1).replace("\\\"", "\""))
801
- } else {
802
- JSONObject(parsed)
803
- }
804
-
805
- if (inner.optString("type") == "custom") {
806
- CarouselOverlayMode.Custom { ShortKitCarouselOverlayBridge(reactApplicationContext) }
807
- } else {
808
- CarouselOverlayMode.None
809
- }
810
- } catch (_: Exception) {
811
- CarouselOverlayMode.None
812
- }
813
- }
814
-
815
- private fun parseFeedInputs(json: String): List<FeedInput>? {
816
- return try {
817
- val arr = JSONArray(json)
818
- val result = mutableListOf<FeedInput>()
819
- for (i in 0 until arr.length()) {
820
- val obj = arr.getJSONObject(i)
821
- when (obj.optString("type")) {
822
- "video" -> {
823
- val playbackId = obj.optString("playbackId", null) ?: continue
824
- result.add(FeedInput.Video(playbackId))
825
- }
826
- "imageCarousel" -> {
827
- val itemObj = obj.optJSONObject("item") ?: continue
828
- val carouselItem = parseImageCarouselItem(itemObj) ?: continue
829
- result.add(FeedInput.ImageCarousel(carouselItem))
830
- }
831
- }
832
- }
833
- result.ifEmpty { null }
834
- } catch (_: Exception) {
835
- null
836
- }
837
- }
838
-
839
- private fun parseImageCarouselItem(obj: JSONObject): ImageCarouselItem? {
840
- val id = obj.optString("id", null) ?: return null
841
- val imagesArr = obj.optJSONArray("images") ?: return null
842
- val images = mutableListOf<CarouselImage>()
843
- for (i in 0 until imagesArr.length()) {
844
- val imgObj = imagesArr.getJSONObject(i)
845
- images.add(CarouselImage(
846
- url = imgObj.getString("url"),
847
- alt = imgObj.optString("alt", null)
848
- ))
849
- }
850
- return ImageCarouselItem(
851
- id = id,
852
- images = images,
853
- autoScrollInterval = if (obj.has("autoScrollInterval")) obj.getDouble("autoScrollInterval") else null,
854
- caption = obj.optString("caption", null),
855
- title = obj.optString("title", null),
856
- description = obj.optString("description", null),
857
- author = obj.optString("author", null),
858
- section = obj.optString("section", null),
859
- articleUrl = obj.optString("articleUrl", null)
860
- )
861
- }
862
-
863
- /**
864
- * Parse optional custom dimensions JSON string into map.
865
- */
866
- private fun parseCustomDimensions(json: String?): Map<String, String>? {
867
- if (json.isNullOrEmpty()) return null
868
- return try {
869
- val obj = JSONObject(json)
870
- val map = mutableMapOf<String, String>()
871
- val keys = obj.keys()
872
- while (keys.hasNext()) {
873
- val key = keys.next()
874
- map[key] = obj.getString(key)
875
- }
876
- map
877
- } catch (_: Exception) {
878
- null
879
- }
292
+ private fun bufferOp(op: () -> Unit) {
293
+ if (pendingBridgeOps == null) pendingBridgeOps = mutableListOf()
294
+ pendingBridgeOps!!.add(op)
880
295
  }
881
296
  }