@shortkitsdk/react-native 0.2.6 → 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.
- package/ShortKitReactNative.podspec +1 -0
- package/android/build.gradle.kts +5 -1
- package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +319 -0
- package/android/src/main/java/com/shortkit/reactnative/ReactLoadingHost.kt +40 -0
- package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +559 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +984 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +88 -220
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +12 -3
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +123 -741
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPlayerNativeView.kt +2 -2
- package/android/src/main/java/com/shortkit/reactnative/ShortKitWidgetNativeView.kt +2 -2
- package/ios/ReactCarouselOverlayHost.swift +177 -0
- package/ios/ReactLoadingHost.swift +38 -0
- package/ios/ReactOverlayHost.swift +458 -0
- package/ios/SKFabricSurfaceWrapper.h +18 -0
- package/ios/SKFabricSurfaceWrapper.mm +57 -0
- package/ios/ShortKitBridge.swift +186 -63
- package/ios/ShortKitFeedView.swift +62 -229
- package/ios/ShortKitFeedViewManager.mm +3 -2
- package/ios/ShortKitModule.mm +66 -37
- package/ios/ShortKitPlayerNativeView.swift +39 -8
- package/ios/ShortKitReactNative-Bridging-Header.h +2 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +2380 -522
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +39 -12
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +39 -12
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +2380 -522
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +39 -12
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +39 -12
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework.bak/Info.plist +43 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +28917 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +824 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +824 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +418 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +16 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +28917 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +824 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +824 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +4 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitWidgetNativeView.swift +3 -3
- package/package.json +1 -1
- package/src/ShortKitCarouselOverlaySurface.tsx +55 -0
- package/src/ShortKitCommands.ts +31 -0
- package/src/ShortKitContext.ts +6 -25
- package/src/ShortKitFeed.tsx +110 -41
- package/src/ShortKitLoadingSurface.tsx +24 -0
- package/src/ShortKitOverlaySurface.tsx +205 -0
- package/src/ShortKitPlayer.tsx +6 -7
- package/src/ShortKitProvider.tsx +27 -286
- package/src/index.ts +5 -3
- package/src/serialization.ts +19 -39
- package/src/specs/NativeShortKitModule.ts +58 -46
- package/src/specs/ShortKitFeedViewNativeComponent.ts +3 -2
- package/src/types.ts +78 -16
- package/src/useShortKit.ts +1 -3
- package/src/useShortKitPlayer.ts +7 -7
- package/android/src/main/java/com/shortkit/reactnative/ShortKitCarouselOverlayBridge.kt +0 -48
- package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +0 -128
- package/ios/ShortKitCarouselOverlayBridge.swift +0 -219
- package/ios/ShortKitOverlayBridge.swift +0 -111
- package/src/CarouselOverlayManager.tsx +0 -70
- package/src/OverlayManager.tsx +0 -87
- package/src/useShortKitCarousel.ts +0 -29
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import com.facebook.react.bridge.Arguments
|
|
6
|
+
import com.facebook.react.bridge.WritableMap
|
|
7
|
+
import com.shortkit.sdk.ShortKit
|
|
8
|
+
import com.shortkit.sdk.ShortKitDelegate
|
|
9
|
+
import com.shortkit.sdk.ShortKitPlayer
|
|
10
|
+
import com.shortkit.sdk.model.CaptionTrack
|
|
11
|
+
import com.shortkit.sdk.model.CarouselImage
|
|
12
|
+
import com.shortkit.sdk.model.ContentItem
|
|
13
|
+
import com.shortkit.sdk.model.ContentSignal
|
|
14
|
+
import com.shortkit.sdk.model.FeedDirection
|
|
15
|
+
import com.shortkit.sdk.model.FeedFilter
|
|
16
|
+
import com.shortkit.sdk.model.FeedInput
|
|
17
|
+
import com.shortkit.sdk.model.FeedScrollPhase
|
|
18
|
+
import com.shortkit.sdk.model.FeedTransitionPhase
|
|
19
|
+
import com.shortkit.sdk.model.ImageCarouselItem
|
|
20
|
+
import com.shortkit.sdk.model.JsonValue
|
|
21
|
+
import com.shortkit.sdk.model.PlayerState
|
|
22
|
+
import com.shortkit.sdk.config.AdOverlayMode
|
|
23
|
+
import com.shortkit.sdk.config.CarouselOverlayMode
|
|
24
|
+
import com.shortkit.sdk.config.FeedConfig
|
|
25
|
+
import com.shortkit.sdk.config.FeedHeight
|
|
26
|
+
import com.shortkit.sdk.config.FeedSource
|
|
27
|
+
import com.shortkit.sdk.config.SurveyOverlayMode
|
|
28
|
+
import com.shortkit.sdk.config.VideoOverlayMode
|
|
29
|
+
import com.shortkit.sdk.feed.ShortKitFeedFragment
|
|
30
|
+
import kotlinx.coroutines.CoroutineScope
|
|
31
|
+
import kotlinx.coroutines.Dispatchers
|
|
32
|
+
import kotlinx.coroutines.SupervisorJob
|
|
33
|
+
import kotlinx.coroutines.cancel
|
|
34
|
+
import kotlinx.coroutines.delay
|
|
35
|
+
import kotlinx.coroutines.launch
|
|
36
|
+
import org.json.JSONArray
|
|
37
|
+
import org.json.JSONObject
|
|
38
|
+
import java.lang.ref.WeakReference
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Android bridge between the ShortKit SDK and the thin TurboModule.
|
|
42
|
+
*
|
|
43
|
+
* Holds the [ShortKit] instance, subscribes to all Kotlin Flows on
|
|
44
|
+
* [ShortKitPlayer], and forwards events to JS via the [emitEvent] lambda.
|
|
45
|
+
*
|
|
46
|
+
* Android equivalent of `react_native_sdk/ios/ShortKitBridge.swift`.
|
|
47
|
+
*/
|
|
48
|
+
class ShortKitBridge(
|
|
49
|
+
apiKey: String,
|
|
50
|
+
context: android.content.Context,
|
|
51
|
+
hasLoadingView: Boolean,
|
|
52
|
+
clientAppName: String?,
|
|
53
|
+
clientAppVersion: String?,
|
|
54
|
+
customDimensionsJSON: String?,
|
|
55
|
+
private val emitEvent: (String, WritableMap) -> Unit
|
|
56
|
+
) {
|
|
57
|
+
|
|
58
|
+
companion object {
|
|
59
|
+
@Volatile
|
|
60
|
+
var shared: ShortKitBridge? = null
|
|
61
|
+
private set
|
|
62
|
+
|
|
63
|
+
/** Feed views waiting for the SDK to be initialized. */
|
|
64
|
+
internal val staticPendingFeedViews = mutableListOf<ShortKitFeedView>()
|
|
65
|
+
|
|
66
|
+
// ------------------------------------------------------------------
|
|
67
|
+
// Static serialization helpers (called by overlay hosts)
|
|
68
|
+
// ------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Serialize a [ContentItem] to a JSON string for bridge transport.
|
|
72
|
+
*/
|
|
73
|
+
fun serializeContentItemToJSON(item: ContentItem): String {
|
|
74
|
+
return try {
|
|
75
|
+
val obj = JSONObject().apply {
|
|
76
|
+
put("id", item.id)
|
|
77
|
+
put("title", item.title)
|
|
78
|
+
item.description?.let { put("description", it) }
|
|
79
|
+
put("duration", item.duration)
|
|
80
|
+
put("streamingUrl", item.streamingUrl)
|
|
81
|
+
put("thumbnailUrl", item.thumbnailUrl)
|
|
82
|
+
put("captionTracks", buildCaptionTracksJSONArray(item.captionTracks))
|
|
83
|
+
item.customMetadata?.let { put("customMetadata", buildCustomMetadataJSONObject(it)) }
|
|
84
|
+
item.author?.let { put("author", it) }
|
|
85
|
+
item.articleUrl?.let { put("articleUrl", it) }
|
|
86
|
+
item.commentCount?.let { put("commentCount", it) }
|
|
87
|
+
item.fallbackUrl?.let { put("fallbackUrl", it) }
|
|
88
|
+
}
|
|
89
|
+
obj.toString()
|
|
90
|
+
} catch (_: Exception) {
|
|
91
|
+
"{}"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Convert a [PlayerState] sealed class to its string representation.
|
|
97
|
+
*/
|
|
98
|
+
fun playerStateString(state: PlayerState): String {
|
|
99
|
+
return when (state) {
|
|
100
|
+
is PlayerState.Idle -> "idle"
|
|
101
|
+
is PlayerState.Loading -> "loading"
|
|
102
|
+
is PlayerState.Ready -> "ready"
|
|
103
|
+
is PlayerState.Playing -> "playing"
|
|
104
|
+
is PlayerState.Paused -> "paused"
|
|
105
|
+
is PlayerState.Seeking -> "seeking"
|
|
106
|
+
is PlayerState.Buffering -> "buffering"
|
|
107
|
+
is PlayerState.Ended -> "ended"
|
|
108
|
+
is PlayerState.Error -> "error"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parse optional custom dimensions JSON string into a map.
|
|
114
|
+
*/
|
|
115
|
+
fun parseCustomDimensions(json: String?): Map<String, String>? {
|
|
116
|
+
if (json.isNullOrEmpty()) return null
|
|
117
|
+
return try {
|
|
118
|
+
val obj = JSONObject(json)
|
|
119
|
+
val map = mutableMapOf<String, String>()
|
|
120
|
+
val keys = obj.keys()
|
|
121
|
+
while (keys.hasNext()) {
|
|
122
|
+
val key = keys.next()
|
|
123
|
+
map[key] = obj.getString(key)
|
|
124
|
+
}
|
|
125
|
+
map
|
|
126
|
+
} catch (_: Exception) {
|
|
127
|
+
null
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Parse the JSON config string from JS into a [FeedConfig].
|
|
133
|
+
*
|
|
134
|
+
* @param json The JSON config string from JS.
|
|
135
|
+
* @param context Android context needed to instantiate overlay views.
|
|
136
|
+
*/
|
|
137
|
+
fun parseFeedConfig(json: String, context: android.content.Context? = null): FeedConfig {
|
|
138
|
+
return try {
|
|
139
|
+
val obj = JSONObject(json)
|
|
140
|
+
|
|
141
|
+
val feedHeight = parseFeedHeight(obj.optString("feedHeight", null))
|
|
142
|
+
val muteOnStart = obj.optBoolean("muteOnStart", true)
|
|
143
|
+
val videoOverlay = parseVideoOverlay(obj.optString("overlay", null), context)
|
|
144
|
+
|
|
145
|
+
val feedSourceStr = obj.optString("feedSource", "algorithmic")
|
|
146
|
+
val feedSource = if (feedSourceStr == "custom") FeedSource.CUSTOM else FeedSource.ALGORITHMIC
|
|
147
|
+
|
|
148
|
+
val carouselOverlay = parseCarouselOverlay(obj.optString("carouselOverlay", null), context)
|
|
149
|
+
val autoplay = obj.optBoolean("autoplay", true)
|
|
150
|
+
val filter = obj.optJSONObject("filter")?.let { parseFeedFilterToModel(it.toString()) }
|
|
151
|
+
|
|
152
|
+
FeedConfig(
|
|
153
|
+
feedHeight = feedHeight,
|
|
154
|
+
videoOverlay = videoOverlay,
|
|
155
|
+
carouselOverlay = carouselOverlay,
|
|
156
|
+
surveyOverlay = SurveyOverlayMode.None,
|
|
157
|
+
adOverlay = AdOverlayMode.None,
|
|
158
|
+
muteOnStart = muteOnStart,
|
|
159
|
+
autoplay = autoplay,
|
|
160
|
+
feedSource = feedSource,
|
|
161
|
+
filter = filter,
|
|
162
|
+
)
|
|
163
|
+
} catch (_: Exception) {
|
|
164
|
+
FeedConfig()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ------------------------------------------------------------------
|
|
169
|
+
// Private static parsing helpers
|
|
170
|
+
// ------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
private fun parseFeedHeight(json: String?): FeedHeight {
|
|
173
|
+
if (json.isNullOrEmpty()) return FeedHeight.Fullscreen
|
|
174
|
+
return try {
|
|
175
|
+
val obj = JSONObject(json)
|
|
176
|
+
when (obj.optString("type")) {
|
|
177
|
+
"percentage" -> {
|
|
178
|
+
val value = obj.optDouble("value", 1.0)
|
|
179
|
+
FeedHeight.Percentage(value.toFloat())
|
|
180
|
+
}
|
|
181
|
+
else -> FeedHeight.Fullscreen
|
|
182
|
+
}
|
|
183
|
+
} catch (_: Exception) {
|
|
184
|
+
FeedHeight.Fullscreen
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Parse a double-stringified overlay JSON.
|
|
190
|
+
* - `"\"none\""` -> None
|
|
191
|
+
* - `"{\"type\":\"custom\"}"` -> Custom with ReactOverlayHost factory
|
|
192
|
+
*/
|
|
193
|
+
private fun parseVideoOverlay(json: String?, context: android.content.Context?): VideoOverlayMode {
|
|
194
|
+
if (json.isNullOrEmpty()) return VideoOverlayMode.None
|
|
195
|
+
return try {
|
|
196
|
+
val parsed = json.trim()
|
|
197
|
+
|
|
198
|
+
if (parsed == "\"none\"" || parsed == "none") {
|
|
199
|
+
return VideoOverlayMode.None
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
val inner = if (parsed.startsWith("\"") && parsed.endsWith("\"")) {
|
|
203
|
+
JSONObject(parsed.substring(1, parsed.length - 1).replace("\\\"", "\""))
|
|
204
|
+
} else {
|
|
205
|
+
JSONObject(parsed)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (inner.optString("type") == "custom" && context != null) {
|
|
209
|
+
val name = inner.optString("name", "Default")
|
|
210
|
+
val ctx = context.applicationContext
|
|
211
|
+
VideoOverlayMode.Custom { ReactOverlayHost(ctx).apply { overlayName = name } }
|
|
212
|
+
} else {
|
|
213
|
+
VideoOverlayMode.None
|
|
214
|
+
}
|
|
215
|
+
} catch (_: Exception) {
|
|
216
|
+
VideoOverlayMode.None
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Parse a double-stringified carousel overlay JSON.
|
|
222
|
+
* - `"\"none\""` -> None
|
|
223
|
+
* - `"{\"type\":\"custom\"}"` -> Custom with ReactCarouselOverlayHost factory
|
|
224
|
+
*/
|
|
225
|
+
private fun parseCarouselOverlay(json: String?, context: android.content.Context?): CarouselOverlayMode {
|
|
226
|
+
if (json.isNullOrEmpty()) return CarouselOverlayMode.None
|
|
227
|
+
return try {
|
|
228
|
+
val parsed = json.trim()
|
|
229
|
+
|
|
230
|
+
if (parsed == "\"none\"" || parsed == "none") {
|
|
231
|
+
return CarouselOverlayMode.None
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
val inner = if (parsed.startsWith("\"") && parsed.endsWith("\"")) {
|
|
235
|
+
JSONObject(parsed.substring(1, parsed.length - 1).replace("\\\"", "\""))
|
|
236
|
+
} else {
|
|
237
|
+
JSONObject(parsed)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (inner.optString("type") == "custom" && context != null) {
|
|
241
|
+
val name = inner.optString("name", "Default")
|
|
242
|
+
val ctx = context.applicationContext
|
|
243
|
+
CarouselOverlayMode.Custom {
|
|
244
|
+
ReactCarouselOverlayHost(ctx).apply {
|
|
245
|
+
carouselOverlayName = name
|
|
246
|
+
// Eagerly create the RN surface so it's mounted and ready
|
|
247
|
+
// before the cell scrolls into view, matching iOS behaviour.
|
|
248
|
+
prepareSurface()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
CarouselOverlayMode.None
|
|
253
|
+
}
|
|
254
|
+
} catch (_: Exception) {
|
|
255
|
+
CarouselOverlayMode.None
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private fun parseFeedFilter(json: String?): String? {
|
|
260
|
+
// On Android, the SDK accepts the raw JSON string for filtering.
|
|
261
|
+
// Return as-is if non-null/non-empty.
|
|
262
|
+
if (json.isNullOrEmpty()) return null
|
|
263
|
+
return json
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Parse a JSON filter string into a FeedFilter model for SDK methods that accept it. */
|
|
267
|
+
fun parseFeedFilterToModel(json: String): FeedFilter? {
|
|
268
|
+
if (json.isBlank()) return null
|
|
269
|
+
return try {
|
|
270
|
+
val obj = JSONObject(json)
|
|
271
|
+
FeedFilter(
|
|
272
|
+
tags = obj.optJSONArray("tags")?.let { arr ->
|
|
273
|
+
(0 until arr.length()).map { arr.getString(it) }
|
|
274
|
+
},
|
|
275
|
+
section = obj.optString("section").ifBlank { null },
|
|
276
|
+
author = obj.optString("author").ifBlank { null },
|
|
277
|
+
contentType = obj.optString("contentType").ifBlank { null },
|
|
278
|
+
metadata = obj.optJSONObject("metadata")?.let { m ->
|
|
279
|
+
m.keys().asSequence().associateWith { m.getString(it) }
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
} catch (_: Exception) { null }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private fun parseFeedInputs(json: String): List<FeedInput>? {
|
|
286
|
+
return try {
|
|
287
|
+
val arr = JSONArray(json)
|
|
288
|
+
val result = mutableListOf<FeedInput>()
|
|
289
|
+
for (i in 0 until arr.length()) {
|
|
290
|
+
val obj = arr.getJSONObject(i)
|
|
291
|
+
when (obj.optString("type")) {
|
|
292
|
+
"video" -> {
|
|
293
|
+
val playbackId = obj.optString("playbackId", null) ?: continue
|
|
294
|
+
val fallbackUrl = obj.optString("fallbackUrl", null)
|
|
295
|
+
result.add(FeedInput.Video(playbackId, fallbackUrl))
|
|
296
|
+
}
|
|
297
|
+
"imageCarousel" -> {
|
|
298
|
+
val itemObj = obj.optJSONObject("item") ?: continue
|
|
299
|
+
val carouselItem = parseImageCarouselItem(itemObj) ?: continue
|
|
300
|
+
result.add(FeedInput.ImageCarousel(carouselItem))
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
result.ifEmpty { null }
|
|
305
|
+
} catch (_: Exception) {
|
|
306
|
+
null
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private fun parseImageCarouselItem(obj: JSONObject): ImageCarouselItem? {
|
|
311
|
+
val id = obj.optString("id", null) ?: return null
|
|
312
|
+
val imagesArr = obj.optJSONArray("images") ?: return null
|
|
313
|
+
val images = mutableListOf<CarouselImage>()
|
|
314
|
+
for (i in 0 until imagesArr.length()) {
|
|
315
|
+
val imgObj = imagesArr.getJSONObject(i)
|
|
316
|
+
images.add(
|
|
317
|
+
CarouselImage(
|
|
318
|
+
url = imgObj.getString("url"),
|
|
319
|
+
alt = imgObj.optString("alt", null)
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
return ImageCarouselItem(
|
|
324
|
+
id = id,
|
|
325
|
+
images = images,
|
|
326
|
+
caption = obj.optString("caption", null),
|
|
327
|
+
title = obj.optString("title", null),
|
|
328
|
+
description = obj.optString("description", null),
|
|
329
|
+
author = obj.optString("author", null),
|
|
330
|
+
section = obj.optString("section", null),
|
|
331
|
+
articleUrl = obj.optString("articleUrl", null)
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private fun buildCaptionTracksJSONArray(tracks: List<CaptionTrack>): JSONArray {
|
|
336
|
+
val arr = JSONArray()
|
|
337
|
+
for (track in tracks) {
|
|
338
|
+
val obj = JSONObject().apply {
|
|
339
|
+
put("language", track.language)
|
|
340
|
+
put("label", track.label)
|
|
341
|
+
put("sourceUrl", track.url ?: "")
|
|
342
|
+
}
|
|
343
|
+
arr.put(obj)
|
|
344
|
+
}
|
|
345
|
+
return arr
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private fun buildCustomMetadataJSONObject(meta: Map<String, JsonValue>): JSONObject {
|
|
349
|
+
val obj = JSONObject()
|
|
350
|
+
for ((key, value) in meta) {
|
|
351
|
+
obj.put(key, jsonValueToAny(value))
|
|
352
|
+
}
|
|
353
|
+
return obj
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private fun jsonValueToAny(value: JsonValue): Any {
|
|
357
|
+
return when (value) {
|
|
358
|
+
is JsonValue.StringValue -> value.value
|
|
359
|
+
is JsonValue.NumberValue -> value.value
|
|
360
|
+
is JsonValue.BoolValue -> value.value
|
|
361
|
+
is JsonValue.ObjectValue -> {
|
|
362
|
+
val obj = JSONObject()
|
|
363
|
+
for ((k, v) in value.value) {
|
|
364
|
+
obj.put(k, jsonValueToAny(v))
|
|
365
|
+
}
|
|
366
|
+
obj
|
|
367
|
+
}
|
|
368
|
+
is JsonValue.NullValue -> JSONObject.NULL
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ------------------------------------------------------------------
|
|
374
|
+
// State
|
|
375
|
+
// ------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
private var shortKit: ShortKit? = null
|
|
378
|
+
private var scope: CoroutineScope? = null
|
|
379
|
+
|
|
380
|
+
/** Preload handles keyed by UUID, consumed by feed views via preloadId prop. */
|
|
381
|
+
var preloadHandles = mutableMapOf<String, Any>()
|
|
382
|
+
private set
|
|
383
|
+
|
|
384
|
+
// ------------------------------------------------------------------
|
|
385
|
+
// Feed instance registry
|
|
386
|
+
// ------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
private val feedRegistry = mutableMapOf<String, WeakReference<ShortKitFeedFragment>>()
|
|
389
|
+
private val pendingOps = mutableMapOf<String, MutableList<(ShortKitFeedFragment) -> Unit>>()
|
|
390
|
+
private val pendingOpsLock = Any()
|
|
391
|
+
|
|
392
|
+
/** Expose the underlying SDK for the Fabric feed view manager. */
|
|
393
|
+
val sdk: ShortKit? get() = shortKit
|
|
394
|
+
|
|
395
|
+
// ------------------------------------------------------------------
|
|
396
|
+
// Init
|
|
397
|
+
// ------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
init {
|
|
400
|
+
val dims = parseCustomDimensions(customDimensionsJSON)
|
|
401
|
+
|
|
402
|
+
val sdk = ShortKit(
|
|
403
|
+
context = context,
|
|
404
|
+
apiKey = apiKey,
|
|
405
|
+
userId = null,
|
|
406
|
+
adProvider = null,
|
|
407
|
+
clientAppName = clientAppName,
|
|
408
|
+
clientAppVersion = clientAppVersion,
|
|
409
|
+
customDimensions = dims
|
|
410
|
+
)
|
|
411
|
+
this.shortKit = sdk
|
|
412
|
+
shared = this
|
|
413
|
+
|
|
414
|
+
// Wire custom loading view provider
|
|
415
|
+
if (hasLoadingView) {
|
|
416
|
+
sdk.loadingViewProvider = { ctx -> ReactLoadingHost(ctx) }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
subscribeToFlows(sdk.player)
|
|
420
|
+
|
|
421
|
+
// Wire delegate
|
|
422
|
+
sdk.delegate = object : ShortKitDelegate {
|
|
423
|
+
override fun onContentTapped(contentId: String, index: Int) {
|
|
424
|
+
val params = Arguments.createMap().apply {
|
|
425
|
+
putString("contentId", contentId)
|
|
426
|
+
putInt("index", index)
|
|
427
|
+
}
|
|
428
|
+
emitEventOnMain("onContentTapped", params)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
override fun onRefreshRequested() {
|
|
432
|
+
emitEventOnMain("onRefreshRequested", Arguments.createMap())
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
override fun onFeedContentFetched(items: List<ContentItem>) {
|
|
436
|
+
val arr = org.json.JSONArray()
|
|
437
|
+
for (item in items) {
|
|
438
|
+
arr.put(org.json.JSONObject(serializeContentItemToJSON(item)))
|
|
439
|
+
}
|
|
440
|
+
val params = Arguments.createMap().apply {
|
|
441
|
+
putString("items", arr.toString())
|
|
442
|
+
}
|
|
443
|
+
emitEventOnMain("onDidFetchContentItems", params)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Drain any feed views that were waiting for the SDK to be ready
|
|
448
|
+
val pending: List<ShortKitFeedView>
|
|
449
|
+
synchronized(staticPendingFeedViews) {
|
|
450
|
+
pending = staticPendingFeedViews.toList()
|
|
451
|
+
staticPendingFeedViews.clear()
|
|
452
|
+
}
|
|
453
|
+
for (view in pending) {
|
|
454
|
+
view.embedFeedFragmentIfNeeded()
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ------------------------------------------------------------------
|
|
459
|
+
// Teardown
|
|
460
|
+
// ------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
fun teardown() {
|
|
463
|
+
scope?.cancel()
|
|
464
|
+
scope = null
|
|
465
|
+
preloadHandles.clear()
|
|
466
|
+
feedRegistry.clear()
|
|
467
|
+
// Note: pendingOps are NOT cleared — they survive re-init
|
|
468
|
+
shortKit?.release()
|
|
469
|
+
shortKit = null
|
|
470
|
+
if (shared === this) {
|
|
471
|
+
shared = null
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ------------------------------------------------------------------
|
|
476
|
+
// Feed registry
|
|
477
|
+
// ------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
fun registerFeedFragment(id: String, fragment: ShortKitFeedFragment) {
|
|
480
|
+
feedRegistry[id] = WeakReference(fragment)
|
|
481
|
+
|
|
482
|
+
// Wire per-feed callbacks
|
|
483
|
+
fragment.onDismiss = {
|
|
484
|
+
emitEventOnMain("onDismiss", Arguments.createMap())
|
|
485
|
+
}
|
|
486
|
+
fragment.onRemainingContentCountChange = { count ->
|
|
487
|
+
val params = Arguments.createMap().apply {
|
|
488
|
+
putString("feedId", id)
|
|
489
|
+
putInt("count", count)
|
|
490
|
+
}
|
|
491
|
+
emitEventOnMain("onRemainingContentCountChanged", params)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Replay buffered operations on the next main-thread tick
|
|
495
|
+
val ops: List<(ShortKitFeedFragment) -> Unit>?
|
|
496
|
+
synchronized(pendingOpsLock) {
|
|
497
|
+
ops = pendingOps.remove(id)?.toList()
|
|
498
|
+
}
|
|
499
|
+
if (ops != null) {
|
|
500
|
+
Handler(Looper.getMainLooper()).post {
|
|
501
|
+
val frag = feedRegistry[id]?.get() ?: return@post
|
|
502
|
+
for (op in ops) {
|
|
503
|
+
op(frag)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
fun unregisterFeedFragment(id: String) {
|
|
510
|
+
feedRegistry.remove(id)
|
|
511
|
+
// pendingOps preserved for detach/reattach cycles
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private fun feedFragment(id: String): ShortKitFeedFragment? {
|
|
515
|
+
return feedRegistry[id]?.get()
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
fun registerFeed(id: String) {
|
|
519
|
+
// No-op — registerFeedFragment handles drain
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
fun unregisterFeed(id: String) {
|
|
523
|
+
// pendingOps preserved for detach/reattach cycles
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Register a feed view that is waiting for the SDK to be initialized. */
|
|
527
|
+
fun registerPendingFeedView(view: ShortKitFeedView) {
|
|
528
|
+
if (shortKit != null) {
|
|
529
|
+
view.embedFeedFragmentIfNeeded()
|
|
530
|
+
} else {
|
|
531
|
+
synchronized(staticPendingFeedViews) {
|
|
532
|
+
staticPendingFeedViews.add(view)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
fun unregisterPendingFeedView(view: ShortKitFeedView) {
|
|
538
|
+
synchronized(staticPendingFeedViews) {
|
|
539
|
+
staticPendingFeedViews.remove(view)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ------------------------------------------------------------------
|
|
544
|
+
// Preload handle management
|
|
545
|
+
// ------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
fun consumePreload(id: String): Any? {
|
|
548
|
+
return preloadHandles.remove(id)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ------------------------------------------------------------------
|
|
552
|
+
// Event emission helpers (public for overlay hosts)
|
|
553
|
+
// ------------------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
fun emitEvent(name: String, params: WritableMap) {
|
|
556
|
+
emitEvent.invoke(name, params)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
fun emitEventOnMain(name: String, params: WritableMap) {
|
|
560
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
561
|
+
emitEvent.invoke(name, params)
|
|
562
|
+
} else {
|
|
563
|
+
Handler(Looper.getMainLooper()).post {
|
|
564
|
+
emitEvent.invoke(name, params)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
fun emitOverlayEvent(name: String, item: ContentItem) {
|
|
570
|
+
val params = Arguments.createMap().apply {
|
|
571
|
+
putString("item", serializeContentItemToJSON(item))
|
|
572
|
+
}
|
|
573
|
+
emitEvent.invoke(name, params)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
fun emitOverlayEvent(name: String, params: WritableMap) {
|
|
577
|
+
emitEvent.invoke(name, params)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
fun emitCarouselOverlayEvent(name: String, params: WritableMap) {
|
|
581
|
+
emitEvent.invoke(name, params)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ------------------------------------------------------------------
|
|
585
|
+
// Player commands
|
|
586
|
+
// ------------------------------------------------------------------
|
|
587
|
+
|
|
588
|
+
// All player commands dispatch to main thread — ExoPlayer is bound to
|
|
589
|
+
// the main looper but @ReactMethod calls arrive on the NativeModules thread.
|
|
590
|
+
|
|
591
|
+
private fun runOnMain(block: () -> Unit) {
|
|
592
|
+
Handler(Looper.getMainLooper()).post(block)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
fun play() { runOnMain { shortKit?.player?.play() } }
|
|
596
|
+
fun pause() { runOnMain { shortKit?.player?.pause() } }
|
|
597
|
+
fun seek(seconds: Double) { runOnMain { shortKit?.player?.seek(seconds) } }
|
|
598
|
+
fun seekAndPlay(seconds: Double) { runOnMain { shortKit?.player?.seekAndPlay(seconds) } }
|
|
599
|
+
fun skipToNext() { runOnMain { shortKit?.player?.skipToNext() } }
|
|
600
|
+
fun skipToPrevious() { runOnMain { shortKit?.player?.skipToPrevious() } }
|
|
601
|
+
fun setMuted(muted: Boolean) { runOnMain { shortKit?.player?.setMuted(muted) } }
|
|
602
|
+
fun setPlaybackRate(rate: Double) { runOnMain { shortKit?.player?.setPlaybackRate(rate.toFloat()) } }
|
|
603
|
+
fun setCaptionsEnabled(enabled: Boolean) { runOnMain { shortKit?.player?.setCaptionsEnabled(enabled) } }
|
|
604
|
+
fun selectCaptionTrack(language: String) { runOnMain { shortKit?.player?.selectCaptionTrack(language) } }
|
|
605
|
+
fun sendContentSignal(signal: String) {
|
|
606
|
+
val contentSignal = if (signal == "positive") ContentSignal.POSITIVE else ContentSignal.NEGATIVE
|
|
607
|
+
runOnMain { shortKit?.player?.sendContentSignal(contentSignal) }
|
|
608
|
+
}
|
|
609
|
+
fun setMaxBitrate(bitrate: Double) { runOnMain { shortKit?.player?.setMaxBitrate(bitrate.toInt()) } }
|
|
610
|
+
|
|
611
|
+
fun setUserId(userId: String) {
|
|
612
|
+
shortKit?.setUserId(userId)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
fun clearUserId() {
|
|
616
|
+
shortKit?.clearUserId()
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
fun onPause() {
|
|
620
|
+
Handler(Looper.getMainLooper()).post { shortKit?.pause() }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
fun onResume() {
|
|
624
|
+
// No-op: let the SDK's internal lifecycle handle resume
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ------------------------------------------------------------------
|
|
628
|
+
// Custom feed operations
|
|
629
|
+
// ------------------------------------------------------------------
|
|
630
|
+
|
|
631
|
+
fun setFeedItems(feedId: String, itemsJSON: String) {
|
|
632
|
+
val fragment = feedFragment(feedId)
|
|
633
|
+
if (fragment != null) {
|
|
634
|
+
val parsed = parseFeedInputs(itemsJSON) ?: return
|
|
635
|
+
Handler(Looper.getMainLooper()).post {
|
|
636
|
+
fragment.setFeedItems(parsed)
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
synchronized(pendingOpsLock) {
|
|
640
|
+
pendingOps.getOrPut(feedId) { mutableListOf() }.add { frag ->
|
|
641
|
+
val parsed = parseFeedInputs(itemsJSON) ?: return@add
|
|
642
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
643
|
+
frag.setFeedItems(parsed)
|
|
644
|
+
} else {
|
|
645
|
+
Handler(Looper.getMainLooper()).post {
|
|
646
|
+
frag.setFeedItems(parsed)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fun appendFeedItems(feedId: String, itemsJSON: String) {
|
|
655
|
+
val fragment = feedFragment(feedId)
|
|
656
|
+
if (fragment != null) {
|
|
657
|
+
val parsed = parseFeedInputs(itemsJSON) ?: return
|
|
658
|
+
Handler(Looper.getMainLooper()).post {
|
|
659
|
+
fragment.appendFeedItems(parsed)
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
synchronized(pendingOpsLock) {
|
|
663
|
+
pendingOps.getOrPut(feedId) { mutableListOf() }.add { frag ->
|
|
664
|
+
val parsed = parseFeedInputs(itemsJSON) ?: return@add
|
|
665
|
+
Handler(Looper.getMainLooper()).post {
|
|
666
|
+
frag.appendFeedItems(parsed)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
fun applyFilter(feedId: String, filterJSON: String?) {
|
|
674
|
+
val fragment = feedRegistry[feedId]?.get()
|
|
675
|
+
if (fragment != null) {
|
|
676
|
+
val filter = filterJSON?.let { parseFeedFilterToModel(it) }
|
|
677
|
+
Handler(Looper.getMainLooper()).post { fragment.applyFilter(filter) }
|
|
678
|
+
} else {
|
|
679
|
+
synchronized(pendingOpsLock) {
|
|
680
|
+
pendingOps.getOrPut(feedId) { mutableListOf() }.add { frag ->
|
|
681
|
+
val filter = filterJSON?.let { parseFeedFilterToModel(it) }
|
|
682
|
+
Handler(Looper.getMainLooper()).post { frag.applyFilter(filter) }
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ------------------------------------------------------------------
|
|
689
|
+
// Fetch content
|
|
690
|
+
// ------------------------------------------------------------------
|
|
691
|
+
|
|
692
|
+
fun fetchContent(limit: Int, filterJSON: String?, callback: (String) -> Unit) {
|
|
693
|
+
val sdk = shortKit
|
|
694
|
+
if (sdk == null) {
|
|
695
|
+
callback("[]")
|
|
696
|
+
return
|
|
697
|
+
}
|
|
698
|
+
scope?.launch {
|
|
699
|
+
try {
|
|
700
|
+
val filter = filterJSON?.let { parseFeedFilterToModel(it) }
|
|
701
|
+
val items = sdk.fetchContent(limit, filter)
|
|
702
|
+
val arr = JSONArray()
|
|
703
|
+
for (item in items) {
|
|
704
|
+
arr.put(JSONObject(serializeContentItemToJSON(item)))
|
|
705
|
+
}
|
|
706
|
+
callback(arr.toString())
|
|
707
|
+
} catch (_: Exception) {
|
|
708
|
+
callback("[]")
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ------------------------------------------------------------------
|
|
714
|
+
// Preload feed
|
|
715
|
+
// ------------------------------------------------------------------
|
|
716
|
+
|
|
717
|
+
fun preloadFeed(configJSON: String, callback: (String) -> Unit) {
|
|
718
|
+
val sdk = shortKit
|
|
719
|
+
if (sdk == null) {
|
|
720
|
+
callback("")
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
val preload = sdk.preloadFeed(filter = configJSON)
|
|
724
|
+
val uuid = java.util.UUID.randomUUID().toString()
|
|
725
|
+
preloadHandles[uuid] = preload
|
|
726
|
+
callback(uuid)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ------------------------------------------------------------------
|
|
730
|
+
// Storyboard / Seek Thumbnails
|
|
731
|
+
// ------------------------------------------------------------------
|
|
732
|
+
|
|
733
|
+
fun prefetchStoryboard(playbackId: String) {
|
|
734
|
+
shortKit?.player?.prefetchStoryboard(playbackId)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
fun getStoryboardData(playbackId: String, callback: (String?) -> Unit) {
|
|
738
|
+
val player = shortKit?.player
|
|
739
|
+
if (player == null) {
|
|
740
|
+
callback(null)
|
|
741
|
+
return
|
|
742
|
+
}
|
|
743
|
+
// Try cached data first
|
|
744
|
+
val json = player.getStoryboardData(playbackId)
|
|
745
|
+
if (json != null) {
|
|
746
|
+
callback(json)
|
|
747
|
+
return
|
|
748
|
+
}
|
|
749
|
+
// Trigger prefetch and retry with coroutine delay (not Thread.sleep)
|
|
750
|
+
player.prefetchStoryboard(playbackId)
|
|
751
|
+
scope?.launch {
|
|
752
|
+
var retries = 0
|
|
753
|
+
while (retries < 30) { // 3 seconds max
|
|
754
|
+
delay(100)
|
|
755
|
+
val data = player.getStoryboardData(playbackId)
|
|
756
|
+
if (data != null) {
|
|
757
|
+
callback(data)
|
|
758
|
+
return@launch
|
|
759
|
+
}
|
|
760
|
+
retries++
|
|
761
|
+
}
|
|
762
|
+
callback(null)
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ------------------------------------------------------------------
|
|
767
|
+
// Content item map (for onCurrentItemChanged)
|
|
768
|
+
// ------------------------------------------------------------------
|
|
769
|
+
|
|
770
|
+
private fun contentItemMap(item: ContentItem): WritableMap {
|
|
771
|
+
return Arguments.createMap().apply {
|
|
772
|
+
putString("id", item.id)
|
|
773
|
+
item.playbackId?.let { putString("playbackId", it) }
|
|
774
|
+
putString("title", item.title)
|
|
775
|
+
item.description?.let { putString("description", it) }
|
|
776
|
+
putDouble("duration", item.duration)
|
|
777
|
+
putString("streamingUrl", item.streamingUrl)
|
|
778
|
+
putString("thumbnailUrl", item.thumbnailUrl)
|
|
779
|
+
|
|
780
|
+
// Caption tracks as JSON string
|
|
781
|
+
putString("captionTracks", buildCaptionTracksJSONArray(item.captionTracks).toString())
|
|
782
|
+
|
|
783
|
+
// Custom metadata as JSON string
|
|
784
|
+
item.customMetadata?.let { meta ->
|
|
785
|
+
putString("customMetadata", buildCustomMetadataJSONObject(meta).toString())
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
item.author?.let { putString("author", it) }
|
|
789
|
+
item.articleUrl?.let { putString("articleUrl", it) }
|
|
790
|
+
item.commentCount?.let { putInt("commentCount", it) }
|
|
791
|
+
item.fallbackUrl?.let { putString("fallbackUrl", it) }
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ------------------------------------------------------------------
|
|
796
|
+
// Flow subscriptions
|
|
797
|
+
// ------------------------------------------------------------------
|
|
798
|
+
|
|
799
|
+
private fun subscribeToFlows(player: ShortKitPlayer) {
|
|
800
|
+
scope?.cancel()
|
|
801
|
+
val newScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
802
|
+
scope = newScope
|
|
803
|
+
|
|
804
|
+
// Player state
|
|
805
|
+
newScope.launch {
|
|
806
|
+
player.playerState.collect { state ->
|
|
807
|
+
val params = Arguments.createMap().apply {
|
|
808
|
+
putString("state", playerStateString(state))
|
|
809
|
+
if (state is PlayerState.Error) {
|
|
810
|
+
putString("errorMessage", state.message)
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
emitEvent.invoke("onPlayerStateChanged", params)
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Current item
|
|
818
|
+
newScope.launch {
|
|
819
|
+
player.currentItem.collect { item ->
|
|
820
|
+
if (item != null) {
|
|
821
|
+
emitEvent.invoke("onCurrentItemChanged", contentItemMap(item))
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Time updates (ms -> seconds)
|
|
827
|
+
newScope.launch {
|
|
828
|
+
player.time.collect { time ->
|
|
829
|
+
val params = Arguments.createMap().apply {
|
|
830
|
+
putDouble("current", time.currentMs / 1000.0)
|
|
831
|
+
putDouble("duration", time.durationMs / 1000.0)
|
|
832
|
+
putDouble("buffered", time.bufferedMs / 1000.0)
|
|
833
|
+
}
|
|
834
|
+
emitEvent.invoke("onTimeUpdate", params)
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Muted state
|
|
839
|
+
newScope.launch {
|
|
840
|
+
player.isMuted.collect { muted ->
|
|
841
|
+
val params = Arguments.createMap().apply {
|
|
842
|
+
putBoolean("isMuted", muted)
|
|
843
|
+
}
|
|
844
|
+
emitEvent.invoke("onMutedChanged", params)
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Playback rate
|
|
849
|
+
newScope.launch {
|
|
850
|
+
player.playbackRate.collect { rate ->
|
|
851
|
+
val params = Arguments.createMap().apply {
|
|
852
|
+
putDouble("rate", rate.toDouble())
|
|
853
|
+
}
|
|
854
|
+
emitEvent.invoke("onPlaybackRateChanged", params)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Captions enabled
|
|
859
|
+
newScope.launch {
|
|
860
|
+
player.captionsEnabled.collect { enabled ->
|
|
861
|
+
val params = Arguments.createMap().apply {
|
|
862
|
+
putBoolean("enabled", enabled)
|
|
863
|
+
}
|
|
864
|
+
emitEvent.invoke("onCaptionsEnabledChanged", params)
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Active caption track
|
|
869
|
+
newScope.launch {
|
|
870
|
+
player.activeCaptionTrack.collect { track ->
|
|
871
|
+
if (track != null) {
|
|
872
|
+
val params = Arguments.createMap().apply {
|
|
873
|
+
putString("language", track.language)
|
|
874
|
+
putString("label", track.label)
|
|
875
|
+
putString("sourceUrl", track.url ?: "")
|
|
876
|
+
}
|
|
877
|
+
emitEvent.invoke("onActiveCaptionTrackChanged", params)
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Active cue (ms -> seconds)
|
|
883
|
+
newScope.launch {
|
|
884
|
+
player.activeCue.collect { cue ->
|
|
885
|
+
if (cue != null) {
|
|
886
|
+
val params = Arguments.createMap().apply {
|
|
887
|
+
putString("text", cue.text)
|
|
888
|
+
putDouble("startTime", cue.startMs / 1000.0)
|
|
889
|
+
putDouble("endTime", cue.endMs / 1000.0)
|
|
890
|
+
}
|
|
891
|
+
emitEvent.invoke("onActiveCueChanged", params)
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Did loop
|
|
897
|
+
newScope.launch {
|
|
898
|
+
player.didLoop.collect { event ->
|
|
899
|
+
val params = Arguments.createMap().apply {
|
|
900
|
+
putString("contentId", event.contentId)
|
|
901
|
+
putInt("loopCount", event.loopCount)
|
|
902
|
+
}
|
|
903
|
+
emitEvent.invoke("onDidLoop", params)
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Feed transition
|
|
908
|
+
newScope.launch {
|
|
909
|
+
player.feedTransition.collect { event ->
|
|
910
|
+
val params = Arguments.createMap().apply {
|
|
911
|
+
putString("phase", when (event.phase) {
|
|
912
|
+
FeedTransitionPhase.BEGAN -> "began"
|
|
913
|
+
FeedTransitionPhase.ENDED -> "ended"
|
|
914
|
+
})
|
|
915
|
+
putString("direction", when (event.direction) {
|
|
916
|
+
FeedDirection.FORWARD -> "forward"
|
|
917
|
+
FeedDirection.BACKWARD -> "backward"
|
|
918
|
+
else -> "forward"
|
|
919
|
+
})
|
|
920
|
+
if (event.from != null) {
|
|
921
|
+
putString("fromItem", serializeContentItemToJSON(event.from!!))
|
|
922
|
+
}
|
|
923
|
+
if (event.to != null) {
|
|
924
|
+
putString("toItem", serializeContentItemToJSON(event.to!!))
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
emitEvent.invoke("onFeedTransition", params)
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Format change
|
|
932
|
+
newScope.launch {
|
|
933
|
+
player.formatChange.collect { event ->
|
|
934
|
+
val params = Arguments.createMap().apply {
|
|
935
|
+
putString("contentId", event.contentId)
|
|
936
|
+
putDouble("fromBitrate", event.fromBitrate.toDouble())
|
|
937
|
+
putDouble("toBitrate", event.toBitrate.toDouble())
|
|
938
|
+
putString("fromResolution", event.fromResolution ?: "")
|
|
939
|
+
putString("toResolution", event.toResolution ?: "")
|
|
940
|
+
}
|
|
941
|
+
emitEvent.invoke("onFormatChange", params)
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Prefetched ahead count
|
|
946
|
+
newScope.launch {
|
|
947
|
+
player.prefetchedAheadCount.collect { count ->
|
|
948
|
+
val params = Arguments.createMap().apply {
|
|
949
|
+
putInt("count", count)
|
|
950
|
+
}
|
|
951
|
+
emitEvent.invoke("onPrefetchedAheadCountChanged", params)
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Remaining content count
|
|
956
|
+
newScope.launch {
|
|
957
|
+
player.remainingContentCount.collect { count ->
|
|
958
|
+
val params = Arguments.createMap().apply {
|
|
959
|
+
putString("feedId", "") // Global fallback — per-feed routing via fragment callback
|
|
960
|
+
putInt("count", count)
|
|
961
|
+
}
|
|
962
|
+
emitEvent.invoke("onRemainingContentCountChanged", params)
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Feed scroll phase
|
|
967
|
+
newScope.launch {
|
|
968
|
+
player.feedScrollPhase.collect { phase ->
|
|
969
|
+
val params = Arguments.createMap().apply {
|
|
970
|
+
when (phase) {
|
|
971
|
+
is FeedScrollPhase.Dragging -> {
|
|
972
|
+
putString("phase", "dragging")
|
|
973
|
+
putString("fromId", phase.fromId)
|
|
974
|
+
}
|
|
975
|
+
is FeedScrollPhase.Settled -> {
|
|
976
|
+
putString("phase", "settled")
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
emitEvent.invoke("onFeedScrollPhase", params)
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|