@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,559 @@
|
|
|
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.view.MotionEvent
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
import com.facebook.react.ReactApplication
|
|
10
|
+
import com.facebook.react.bridge.Arguments
|
|
11
|
+
import com.facebook.react.interfaces.fabric.ReactSurface
|
|
12
|
+
import com.facebook.react.runtime.ReactSurfaceImpl
|
|
13
|
+
import com.shortkit.sdk.ShortKitPlayer
|
|
14
|
+
import com.shortkit.sdk.VttCue
|
|
15
|
+
import com.shortkit.sdk.model.ContentItem
|
|
16
|
+
import com.shortkit.sdk.model.FeedScrollPhase
|
|
17
|
+
import com.shortkit.sdk.model.PlayerState
|
|
18
|
+
import com.shortkit.sdk.overlay.FeedOverlay
|
|
19
|
+
import kotlinx.coroutines.CoroutineScope
|
|
20
|
+
import kotlinx.coroutines.Dispatchers
|
|
21
|
+
import kotlinx.coroutines.SupervisorJob
|
|
22
|
+
import kotlinx.coroutines.cancel
|
|
23
|
+
import kotlinx.coroutines.launch
|
|
24
|
+
import org.json.JSONObject
|
|
25
|
+
import java.util.UUID
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A [FrameLayout] that conforms to [FeedOverlay] and hosts a React Native
|
|
29
|
+
* Fabric [ReactSurface] for rendering the developer's React overlay component
|
|
30
|
+
* inside a feed cell.
|
|
31
|
+
*
|
|
32
|
+
* Each cell gets its own instance (via the overlay factory). The surface is
|
|
33
|
+
* created eagerly on the first [attach] call and reused across cell reuse cycles.
|
|
34
|
+
*
|
|
35
|
+
* Surface properties are set ONCE per item (in [configure]) to provide initial
|
|
36
|
+
* values. All subsequent dynamic state changes flow through native module events
|
|
37
|
+
* routed by [surfaceId], triggering React re-renders instead of Fabric remounts.
|
|
38
|
+
*
|
|
39
|
+
* Android equivalent of `react_native_sdk/ios/ReactOverlayHost.swift`.
|
|
40
|
+
*/
|
|
41
|
+
class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
42
|
+
|
|
43
|
+
private companion object {
|
|
44
|
+
const val TAG = "SK:OverlayHost"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ------------------------------------------------------------------
|
|
48
|
+
// Configuration
|
|
49
|
+
// ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/** Suffix for the surface module name (e.g. "news" -> "ShortKitOverlay_news"). */
|
|
52
|
+
var overlayName: String = "Default"
|
|
53
|
+
|
|
54
|
+
/** Unique identifier for this overlay instance, used for event routing. */
|
|
55
|
+
val surfaceId: String = UUID.randomUUID().toString()
|
|
56
|
+
|
|
57
|
+
// ------------------------------------------------------------------
|
|
58
|
+
// Fabric layout workaround
|
|
59
|
+
// ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
private val layoutHandler = Handler(Looper.getMainLooper())
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fabric may suppress layout propagation to native child views inside
|
|
65
|
+
* the SDK's RecyclerView cells. Override to force a manual layout pass.
|
|
66
|
+
*/
|
|
67
|
+
override fun requestLayout() {
|
|
68
|
+
super.requestLayout()
|
|
69
|
+
@Suppress("UNNECESSARY_SAFE_CALL")
|
|
70
|
+
layoutHandler?.post {
|
|
71
|
+
measure(
|
|
72
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
73
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
74
|
+
)
|
|
75
|
+
layout(left, top, right, bottom)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ------------------------------------------------------------------
|
|
80
|
+
// State
|
|
81
|
+
// ------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
private var surface: ReactSurface? = null
|
|
84
|
+
private var player: ShortKitPlayer? = null
|
|
85
|
+
private var currentItem: ContentItem? = null
|
|
86
|
+
private var isActive: Boolean = false
|
|
87
|
+
|
|
88
|
+
// Player state cache — always updated by flow subscriptions,
|
|
89
|
+
// but only emitted to JS when isActive == true.
|
|
90
|
+
private var cachedPlayerState: String = "idle"
|
|
91
|
+
private var cachedIsMuted: Boolean = true
|
|
92
|
+
private var cachedPlaybackRate: Double = 1.0
|
|
93
|
+
private var cachedCaptionsEnabled: Boolean = false
|
|
94
|
+
private var cachedActiveCue: JSONObject? = null
|
|
95
|
+
private var cachedFeedScrollPhase: String? = null
|
|
96
|
+
|
|
97
|
+
// Time coalescing (250ms)
|
|
98
|
+
private var cachedCurrentTime: Double = 0.0
|
|
99
|
+
private var cachedDuration: Double = 0.0
|
|
100
|
+
private var cachedBuffered: Double = 0.0
|
|
101
|
+
private var timeDirty: Boolean = false
|
|
102
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
103
|
+
private var timeCoalesceRunnable: Runnable? = null
|
|
104
|
+
|
|
105
|
+
private var flowScope: CoroutineScope? = null
|
|
106
|
+
|
|
107
|
+
// ------------------------------------------------------------------
|
|
108
|
+
// Init
|
|
109
|
+
// ------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/** Height of the scrubber touch area in dp (matches styles.scrubberTouchArea). */
|
|
112
|
+
private val scrubberTouchHeightPx: Int
|
|
113
|
+
get() = (40 * resources.displayMetrics.density).toInt()
|
|
114
|
+
|
|
115
|
+
init {
|
|
116
|
+
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ------------------------------------------------------------------
|
|
120
|
+
// Touch: prevent feed scroll when scrubbing
|
|
121
|
+
// ------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* When the user touches the bottom scrubber zone, immediately tell the
|
|
125
|
+
* parent RecyclerView to stop intercepting touch events. This prevents
|
|
126
|
+
* vertical feed swipes from stealing the horizontal scrub gesture.
|
|
127
|
+
*
|
|
128
|
+
* Must be done in native (not JS) because RecyclerView's
|
|
129
|
+
* onInterceptTouchEvent fires before the RN touch system processes
|
|
130
|
+
* the PanResponder grant.
|
|
131
|
+
*/
|
|
132
|
+
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
133
|
+
if (ev.action == MotionEvent.ACTION_DOWN && height > 0) {
|
|
134
|
+
if (ev.y > height - scrubberTouchHeightPx) {
|
|
135
|
+
parent?.requestDisallowInterceptTouchEvent(true)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return super.onInterceptTouchEvent(ev)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ------------------------------------------------------------------
|
|
142
|
+
// FeedOverlay
|
|
143
|
+
// ------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
override fun attach(player: ShortKitPlayer) {
|
|
146
|
+
this.player = player
|
|
147
|
+
resubscribeToPlayer(player)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override fun configure(item: ContentItem) {
|
|
151
|
+
currentItem = item
|
|
152
|
+
isActive = false
|
|
153
|
+
timeDirty = false
|
|
154
|
+
stopTimeCoalescing()
|
|
155
|
+
|
|
156
|
+
// Reset ALL cached state so recycled cells don't flash stale values.
|
|
157
|
+
// Mirrors iOS ReactOverlayHost.configure(with:).
|
|
158
|
+
cachedCurrentTime = 0.0
|
|
159
|
+
cachedDuration = 0.0
|
|
160
|
+
cachedBuffered = 0.0
|
|
161
|
+
cachedPlayerState = "idle"
|
|
162
|
+
cachedActiveCue = null
|
|
163
|
+
cachedFeedScrollPhase = null
|
|
164
|
+
|
|
165
|
+
if (surface == null) {
|
|
166
|
+
createSurfaceIfNeeded()
|
|
167
|
+
} else {
|
|
168
|
+
(surface as? ReactSurfaceImpl)?.updateInitProps(buildInitialPropsBundle())
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Pre-size the surface view NOW — before the overlay is attached to a cell.
|
|
172
|
+
// Eliminates the black flash on cell display.
|
|
173
|
+
val parentView = parent as? android.view.View
|
|
174
|
+
val w = if (width > 0) width
|
|
175
|
+
else if (parentView != null && parentView.width > 0) parentView.width
|
|
176
|
+
else context.resources.displayMetrics.widthPixels
|
|
177
|
+
val h = if (height > 0) height
|
|
178
|
+
else if (parentView != null && parentView.height > 0) parentView.height
|
|
179
|
+
else context.resources.displayMetrics.heightPixels
|
|
180
|
+
measureAndLayoutSurfaceView(w, h)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
override fun activatePlayback() {
|
|
184
|
+
isActive = true
|
|
185
|
+
startTimeCoalescing()
|
|
186
|
+
|
|
187
|
+
// Defer the event burst to the next tick. The JS surface needs time to
|
|
188
|
+
// mount and establish event subscriptions (useEffect runs after render).
|
|
189
|
+
// Mirrors iOS: DispatchQueue.main.async { self.emitFullState() }
|
|
190
|
+
handler.post {
|
|
191
|
+
if (isActive) {
|
|
192
|
+
emitFullState()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
override fun fadeOutForTransition() {
|
|
198
|
+
val item = currentItem ?: return
|
|
199
|
+
ShortKitBridge.shared?.emitOverlayEvent("onOverlayFadeOut", item)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
override fun restoreFromTransition() {
|
|
203
|
+
val item = currentItem ?: return
|
|
204
|
+
ShortKitBridge.shared?.emitOverlayEvent("onOverlayRestore", item)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ------------------------------------------------------------------
|
|
208
|
+
// View lifecycle
|
|
209
|
+
// ------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
override fun onAttachedToWindow() {
|
|
212
|
+
super.onAttachedToWindow()
|
|
213
|
+
|
|
214
|
+
// Re-subscribe to player flows if we were detached and reattached
|
|
215
|
+
// (RecyclerView cell recycling). onDetachedFromWindow cancels flowScope.
|
|
216
|
+
if (flowScope == null) {
|
|
217
|
+
player?.let { resubscribeToPlayer(it) }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Force self-measurement using parent dimensions. The SDK adds us
|
|
221
|
+
// with MATCH_PARENT but RecyclerView may not have laid out the cell yet.
|
|
222
|
+
val parentView = parent as? android.view.View
|
|
223
|
+
if (parentView != null && parentView.width > 0 && parentView.height > 0 && (width == 0 || height == 0)) {
|
|
224
|
+
val wSpec = MeasureSpec.makeMeasureSpec(parentView.width, MeasureSpec.EXACTLY)
|
|
225
|
+
val hSpec = MeasureSpec.makeMeasureSpec(parentView.height, MeasureSpec.EXACTLY)
|
|
226
|
+
measure(wSpec, hSpec)
|
|
227
|
+
layout(0, 0, parentView.width, parentView.height)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
override fun onDetachedFromWindow() {
|
|
232
|
+
super.onDetachedFromWindow()
|
|
233
|
+
// Cancel flow subscriptions to avoid emitting events for off-screen cells.
|
|
234
|
+
// Do NOT stop the surface — keep JS alive so subscriptions persist across
|
|
235
|
+
// RecyclerView detach/reattach cycles. This matches iOS behavior where the
|
|
236
|
+
// surface is never stopped (UICollectionView doesn't detach from window).
|
|
237
|
+
flowScope?.cancel()
|
|
238
|
+
flowScope = null
|
|
239
|
+
isActive = false
|
|
240
|
+
stopTimeCoalescing()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
244
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
245
|
+
if (w > 0 && h > 0) {
|
|
246
|
+
measureAndLayoutSurfaceView(w, h)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Continuously enforce size on the ReactSurfaceView child.
|
|
252
|
+
*
|
|
253
|
+
* ReactSurfaceView.onMeasure only sizes correctly with EXACTLY mode
|
|
254
|
+
* specs. Fabric's async layout may reset the surface view to 0x0 after
|
|
255
|
+
* our initial measure. Override onLayout to re-push the correct
|
|
256
|
+
* dimensions on every layout pass — mirrors iOS's layoutSubviews()
|
|
257
|
+
* calling setMinimumSize/setMaximumSize in ReactOverlayHost.swift.
|
|
258
|
+
*/
|
|
259
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
260
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
261
|
+
val w = right - left
|
|
262
|
+
val h = bottom - top
|
|
263
|
+
if (w > 0 && h > 0) {
|
|
264
|
+
measureAndLayoutSurfaceView(w, h)
|
|
265
|
+
} else {
|
|
266
|
+
// Host still 0x0 — try parent dimensions
|
|
267
|
+
val parentView = parent as? android.view.View
|
|
268
|
+
val pw = parentView?.width ?: 0
|
|
269
|
+
val ph = parentView?.height ?: 0
|
|
270
|
+
if (pw > 0 && ph > 0) {
|
|
271
|
+
measureAndLayoutSurfaceView(pw, ph)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private fun measureAndLayoutSurfaceView(w: Int, h: Int) {
|
|
277
|
+
val sv = surface?.view ?: return
|
|
278
|
+
if (sv.width == w && sv.height == h) return // already correct
|
|
279
|
+
val wSpec = MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY)
|
|
280
|
+
val hSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY)
|
|
281
|
+
sv.measure(wSpec, hSpec)
|
|
282
|
+
sv.layout(0, 0, w, h)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ------------------------------------------------------------------
|
|
286
|
+
// Surface Creation
|
|
287
|
+
// ------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
private fun createSurfaceIfNeeded() {
|
|
290
|
+
if (surface != null) return
|
|
291
|
+
|
|
292
|
+
val reactHost = (context.applicationContext as? ReactApplication)?.reactHost
|
|
293
|
+
if (reactHost == null) {
|
|
294
|
+
android.util.Log.e(TAG, "[$surfaceId] createSurface FAILED: reactHost is null")
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
val moduleName = "ShortKitOverlay_$overlayName"
|
|
298
|
+
val initialProps = buildInitialPropsBundle()
|
|
299
|
+
|
|
300
|
+
val newSurface = reactHost.createSurface(context, moduleName, initialProps)
|
|
301
|
+
surface = newSurface
|
|
302
|
+
|
|
303
|
+
val surfaceView = newSurface.view
|
|
304
|
+
if (surfaceView != null) {
|
|
305
|
+
surfaceView.layoutParams = LayoutParams(
|
|
306
|
+
LayoutParams.MATCH_PARENT,
|
|
307
|
+
LayoutParams.MATCH_PARENT
|
|
308
|
+
)
|
|
309
|
+
addView(surfaceView)
|
|
310
|
+
} else {
|
|
311
|
+
android.util.Log.e(TAG, "[$surfaceId] createSurface: surfaceView is NULL")
|
|
312
|
+
}
|
|
313
|
+
newSurface.start()
|
|
314
|
+
|
|
315
|
+
// The host may still be 0x0 at this point (RecyclerView hasn't laid
|
|
316
|
+
// out the cell yet). Use the parent's dimensions if available.
|
|
317
|
+
val parentView = parent as? android.view.View
|
|
318
|
+
val w = if (width > 0) width else parentView?.width ?: 0
|
|
319
|
+
val h = if (height > 0) height else parentView?.height ?: 0
|
|
320
|
+
if (w > 0 && h > 0) {
|
|
321
|
+
measureAndLayoutSurfaceView(w, h)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Build a Bundle with the full initial state for the surface.
|
|
327
|
+
* Mirrors iOS's pushInitialProperties().
|
|
328
|
+
*/
|
|
329
|
+
private fun buildInitialPropsBundle(): Bundle = Bundle().apply {
|
|
330
|
+
putString("surfaceId", surfaceId)
|
|
331
|
+
putBoolean("isActive", false)
|
|
332
|
+
putString("playerState", "idle")
|
|
333
|
+
putBoolean("isMuted", cachedIsMuted)
|
|
334
|
+
putDouble("playbackRate", cachedPlaybackRate)
|
|
335
|
+
putBoolean("captionsEnabled", cachedCaptionsEnabled)
|
|
336
|
+
currentItem?.let { item ->
|
|
337
|
+
putString("item", ShortKitBridge.serializeContentItemToJSON(item))
|
|
338
|
+
}
|
|
339
|
+
cachedActiveCue?.let { cue ->
|
|
340
|
+
putString("activeCue", cue.toString())
|
|
341
|
+
}
|
|
342
|
+
cachedFeedScrollPhase?.let { putString("feedScrollPhase", it) }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ------------------------------------------------------------------
|
|
346
|
+
// Player Subscriptions
|
|
347
|
+
// ------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
private fun resubscribeToPlayer(player: ShortKitPlayer) {
|
|
350
|
+
flowScope?.cancel()
|
|
351
|
+
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
352
|
+
flowScope = scope
|
|
353
|
+
|
|
354
|
+
// Player state
|
|
355
|
+
scope.launch {
|
|
356
|
+
player.playerState.collect { state ->
|
|
357
|
+
cachedPlayerState = ShortKitBridge.playerStateString(state)
|
|
358
|
+
if (isActive) {
|
|
359
|
+
val params = Arguments.createMap().apply {
|
|
360
|
+
putString("surfaceId", surfaceId)
|
|
361
|
+
putString("playerState", cachedPlayerState)
|
|
362
|
+
}
|
|
363
|
+
ShortKitBridge.shared?.emitEvent("onOverlayPlayerStateChanged", params)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Muted
|
|
369
|
+
scope.launch {
|
|
370
|
+
player.isMuted.collect { muted ->
|
|
371
|
+
cachedIsMuted = muted
|
|
372
|
+
if (isActive) {
|
|
373
|
+
val params = Arguments.createMap().apply {
|
|
374
|
+
putString("surfaceId", surfaceId)
|
|
375
|
+
putBoolean("isMuted", cachedIsMuted)
|
|
376
|
+
}
|
|
377
|
+
ShortKitBridge.shared?.emitEvent("onOverlayMutedChanged", params)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Playback rate
|
|
383
|
+
scope.launch {
|
|
384
|
+
player.playbackRate.collect { rate ->
|
|
385
|
+
cachedPlaybackRate = rate.toDouble()
|
|
386
|
+
if (isActive) {
|
|
387
|
+
val params = Arguments.createMap().apply {
|
|
388
|
+
putString("surfaceId", surfaceId)
|
|
389
|
+
putDouble("playbackRate", cachedPlaybackRate)
|
|
390
|
+
}
|
|
391
|
+
ShortKitBridge.shared?.emitEvent("onOverlayPlaybackRateChanged", params)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Captions enabled
|
|
397
|
+
scope.launch {
|
|
398
|
+
player.captionsEnabled.collect { enabled ->
|
|
399
|
+
cachedCaptionsEnabled = enabled
|
|
400
|
+
if (isActive) {
|
|
401
|
+
val params = Arguments.createMap().apply {
|
|
402
|
+
putString("surfaceId", surfaceId)
|
|
403
|
+
putBoolean("captionsEnabled", cachedCaptionsEnabled)
|
|
404
|
+
}
|
|
405
|
+
ShortKitBridge.shared?.emitEvent("onOverlayCaptionsEnabledChanged", params)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Active cue (ms -> seconds)
|
|
411
|
+
scope.launch {
|
|
412
|
+
player.activeCue.collect { cue ->
|
|
413
|
+
cachedActiveCue = if (cue != null) {
|
|
414
|
+
JSONObject().apply {
|
|
415
|
+
put("text", cue.text)
|
|
416
|
+
put("startTime", cue.startMs / 1000.0)
|
|
417
|
+
put("endTime", cue.endMs / 1000.0)
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
null
|
|
421
|
+
}
|
|
422
|
+
if (isActive) {
|
|
423
|
+
val params = Arguments.createMap().apply {
|
|
424
|
+
putString("surfaceId", surfaceId)
|
|
425
|
+
val cueJson = cachedActiveCue?.toString()
|
|
426
|
+
if (cueJson != null) {
|
|
427
|
+
putString("activeCue", cueJson)
|
|
428
|
+
} else {
|
|
429
|
+
putNull("activeCue")
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
ShortKitBridge.shared?.emitEvent("onOverlayActiveCueChanged", params)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Time (ms -> seconds, coalesced via handler)
|
|
438
|
+
scope.launch {
|
|
439
|
+
player.time.collect { time ->
|
|
440
|
+
cachedCurrentTime = time.currentMs / 1000.0
|
|
441
|
+
cachedDuration = time.durationMs / 1000.0
|
|
442
|
+
cachedBuffered = time.bufferedMs / 1000.0
|
|
443
|
+
if (isActive) {
|
|
444
|
+
timeDirty = true
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Feed scroll phase
|
|
450
|
+
scope.launch {
|
|
451
|
+
player.feedScrollPhase.collect { phase ->
|
|
452
|
+
cachedFeedScrollPhase = when (phase) {
|
|
453
|
+
is FeedScrollPhase.Dragging -> JSONObject().apply {
|
|
454
|
+
put("phase", "dragging")
|
|
455
|
+
put("fromId", phase.fromId)
|
|
456
|
+
}.toString()
|
|
457
|
+
is FeedScrollPhase.Settled -> """{"phase":"settled"}"""
|
|
458
|
+
}
|
|
459
|
+
if (isActive) {
|
|
460
|
+
val params = Arguments.createMap().apply {
|
|
461
|
+
putString("surfaceId", surfaceId)
|
|
462
|
+
putString("feedScrollPhase", cachedFeedScrollPhase)
|
|
463
|
+
}
|
|
464
|
+
ShortKitBridge.shared?.emitEvent("onOverlayFeedScrollPhaseChanged", params)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ------------------------------------------------------------------
|
|
471
|
+
// Time Coalescing (250ms)
|
|
472
|
+
// ------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
private fun startTimeCoalescing() {
|
|
475
|
+
stopTimeCoalescing()
|
|
476
|
+
val runnable = object : Runnable {
|
|
477
|
+
override fun run() {
|
|
478
|
+
// Suppress time updates while seeking. On iOS, AVPlayer's time
|
|
479
|
+
// observer naturally stops firing during seeks. On Android,
|
|
480
|
+
// ExoPlayer's time flow keeps emitting the stale pre-seek
|
|
481
|
+
// position, causing the progress bar to oscillate between the
|
|
482
|
+
// seek target and the old position. Skip until seek completes.
|
|
483
|
+
if (isActive && timeDirty && cachedPlayerState != "seeking") {
|
|
484
|
+
emitTimeUpdate()
|
|
485
|
+
timeDirty = false
|
|
486
|
+
}
|
|
487
|
+
if (isActive) {
|
|
488
|
+
handler.postDelayed(this, 250)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
timeCoalesceRunnable = runnable
|
|
493
|
+
handler.postDelayed(runnable, 250)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private fun stopTimeCoalescing() {
|
|
497
|
+
timeCoalesceRunnable?.let { handler.removeCallbacks(it) }
|
|
498
|
+
timeCoalesceRunnable = null
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private fun emitTimeUpdate() {
|
|
502
|
+
val params = Arguments.createMap().apply {
|
|
503
|
+
putString("surfaceId", surfaceId)
|
|
504
|
+
putDouble("current", cachedCurrentTime)
|
|
505
|
+
putDouble("duration", cachedDuration)
|
|
506
|
+
putDouble("buffered", cachedBuffered)
|
|
507
|
+
}
|
|
508
|
+
ShortKitBridge.shared?.emitEvent("onOverlayTimeUpdate", params)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ------------------------------------------------------------------
|
|
512
|
+
// Full State Emission
|
|
513
|
+
// ------------------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Emit all cached state as individual events. Called from [activatePlayback]
|
|
517
|
+
* (deferred) to synchronize JS state after cell reuse.
|
|
518
|
+
*/
|
|
519
|
+
private fun emitFullState() {
|
|
520
|
+
val bridge = ShortKitBridge.shared ?: return
|
|
521
|
+
|
|
522
|
+
bridge.emitEvent("onOverlayActiveChanged", Arguments.createMap().apply {
|
|
523
|
+
putString("surfaceId", surfaceId)
|
|
524
|
+
putBoolean("isActive", true)
|
|
525
|
+
})
|
|
526
|
+
bridge.emitEvent("onOverlayPlayerStateChanged", Arguments.createMap().apply {
|
|
527
|
+
putString("surfaceId", surfaceId)
|
|
528
|
+
putString("playerState", cachedPlayerState)
|
|
529
|
+
})
|
|
530
|
+
bridge.emitEvent("onOverlayMutedChanged", Arguments.createMap().apply {
|
|
531
|
+
putString("surfaceId", surfaceId)
|
|
532
|
+
putBoolean("isMuted", cachedIsMuted)
|
|
533
|
+
})
|
|
534
|
+
bridge.emitEvent("onOverlayPlaybackRateChanged", Arguments.createMap().apply {
|
|
535
|
+
putString("surfaceId", surfaceId)
|
|
536
|
+
putDouble("playbackRate", cachedPlaybackRate)
|
|
537
|
+
})
|
|
538
|
+
bridge.emitEvent("onOverlayCaptionsEnabledChanged", Arguments.createMap().apply {
|
|
539
|
+
putString("surfaceId", surfaceId)
|
|
540
|
+
putBoolean("captionsEnabled", cachedCaptionsEnabled)
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
bridge.emitEvent("onOverlayActiveCueChanged", Arguments.createMap().apply {
|
|
544
|
+
putString("surfaceId", surfaceId)
|
|
545
|
+
val cueJson = cachedActiveCue?.toString()
|
|
546
|
+
if (cueJson != null) {
|
|
547
|
+
putString("activeCue", cueJson)
|
|
548
|
+
} else {
|
|
549
|
+
putNull("activeCue")
|
|
550
|
+
}
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
bridge.emitEvent("onOverlayFeedScrollPhaseChanged", Arguments.createMap().apply {
|
|
554
|
+
putString("surfaceId", surfaceId)
|
|
555
|
+
cachedFeedScrollPhase?.let { putString("feedScrollPhase", it) }
|
|
556
|
+
?: putNull("feedScrollPhase")
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
}
|