@shortkitsdk/react-native 0.2.5 → 0.2.11

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