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