@shortkitsdk/react-native 0.2.12 → 0.2.15
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 +117 -2
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +7 -5
- package/ios/ReactCarouselOverlayHost.swift +67 -35
- package/ios/ReactOverlayHost.swift +85 -75
- package/ios/ReactVideoCarouselOverlayHost.swift +283 -0
- package/ios/ShortKitBridge.swift +122 -20
- package/ios/ShortKitModule.mm +15 -5
- package/ios/ShortKitSDK.xcframework/Info.plist +5 -4
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +5 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +2488 -281
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +65 -5
- 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 +65 -5
- 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.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +2 -1
- package/ios/ShortKitSDK.xcframework/{ios-arm64-simulator → ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Info.plist +5 -1
- package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +8233 -3592
- package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +120 -19
- package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/{ShortKitSDK.xcframework.bak/ios-arm64-simulator → ShortKitSDK.xcframework/ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +120 -19
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +33558 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +925 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +925 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +212 -0
- package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +1 -1
- package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +7338 -3272
- package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +106 -15
- package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +106 -15
- package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
- package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +1838 -206
- package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +51 -1
- package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +51 -1
- package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework.dev-backup/ios-arm64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +168 -0
- package/package.json +1 -1
- package/src/ShortKitCarouselOverlaySurface.tsx +57 -2
- package/src/ShortKitContext.ts +11 -0
- package/src/ShortKitFeed.tsx +25 -10
- package/src/ShortKitOverlaySurface.tsx +24 -11
- package/src/ShortKitProvider.tsx +4 -2
- package/src/ShortKitVideoCarouselOverlaySurface.tsx +156 -0
- package/src/ShortKitWidget.tsx +3 -3
- package/src/index.ts +5 -1
- package/src/serialization.ts +7 -0
- package/src/specs/NativeShortKitModule.ts +18 -2
- package/src/types.ts +48 -3
- package/ios/ShortKitSDK.xcframework/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework.bak/ios-arm64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/{ios-arm64-simulator → ios-arm64_x86_64-simulator}/ShortKitSDK.framework/Modules/module.modulemap +0 -0
- package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/Info.plist +4 -4
- /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Info.plist +0 -0
- /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64/ShortKitSDK.framework/Modules/module.modulemap +0 -0
- /package/ios/{ShortKitSDK.xcframework → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Headers/ShortKitSDK-Swift.h +0 -0
- /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Info.plist +0 -0
- /package/ios/{ShortKitSDK.xcframework.bak → ShortKitSDK.xcframework.dev-backup}/ios-arm64-simulator/ShortKitSDK.framework/Modules/module.modulemap +0 -0
|
@@ -46,7 +46,9 @@ import ShortKitSDK
|
|
|
46
46
|
private var cachedPlaybackRate: Double = 1.0
|
|
47
47
|
private var cachedCaptionsEnabled: Bool = false
|
|
48
48
|
private var cachedActiveCue: [String: Any]? = nil
|
|
49
|
-
private var
|
|
49
|
+
private var cachedActiveCueJson: String? = nil // pre-serialized for bridge
|
|
50
|
+
private var cachedFeedScrollPhase: String? = nil // already a JSON string
|
|
51
|
+
private var isDragging: Bool = false // true while scroll phase is .dragging
|
|
50
52
|
|
|
51
53
|
// Time coalescing
|
|
52
54
|
private var cachedTime: (current: Double, duration: Double, buffered: Double) = (0, 0, 0)
|
|
@@ -126,6 +128,7 @@ import ShortKitSDK
|
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
public func configure(with item: ContentItem) {
|
|
131
|
+
let isSameItem = item.id == currentItem?.id
|
|
129
132
|
currentItem = item
|
|
130
133
|
isActive = false
|
|
131
134
|
timeDirty = false
|
|
@@ -136,14 +139,18 @@ import ShortKitSDK
|
|
|
136
139
|
cachedTime = (0, 0, 0)
|
|
137
140
|
cachedPlayerState = "idle"
|
|
138
141
|
cachedActiveCue = nil
|
|
142
|
+
cachedActiveCueJson = nil
|
|
139
143
|
cachedFeedScrollPhase = nil
|
|
144
|
+
isDragging = false
|
|
140
145
|
|
|
141
146
|
createSurfaceIfNeeded()
|
|
142
147
|
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
148
|
+
// Only push properties if the surface already exists.
|
|
149
|
+
// If the surface is still being created asynchronously,
|
|
150
|
+
// installSurface() will call pushInitialProperties() when ready.
|
|
151
|
+
if surface != nil && !isSameItem {
|
|
152
|
+
pushInitialProperties()
|
|
153
|
+
}
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
public func activatePlayback() {
|
|
@@ -162,30 +169,21 @@ import ShortKitSDK
|
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
171
|
|
|
165
|
-
/// Emit all cached state as
|
|
166
|
-
/// (deferred) and
|
|
172
|
+
/// Emit all cached state as a single batched event. Called from
|
|
173
|
+
/// activatePlayback() (deferred) and on drag→settled transitions.
|
|
174
|
+
/// Uses pre-serialized JSON strings to avoid JSONSerialization on the
|
|
175
|
+
/// main thread during the swipe-settle critical path.
|
|
167
176
|
private func emitFullState() {
|
|
168
|
-
|
|
177
|
+
bridge?.emit("onOverlayFullState", body: [
|
|
169
178
|
"surfaceId": surfaceId,
|
|
170
179
|
"isActive": true,
|
|
171
180
|
"playerState": cachedPlayerState,
|
|
172
181
|
"isMuted": cachedIsMuted,
|
|
173
182
|
"playbackRate": cachedPlaybackRate,
|
|
174
183
|
"captionsEnabled": cachedCaptionsEnabled,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
let cueJson = String(data: cueData, encoding: .utf8) {
|
|
179
|
-
body["activeCue"] = cueJson
|
|
180
|
-
} else {
|
|
181
|
-
body["activeCue"] = NSNull()
|
|
182
|
-
}
|
|
183
|
-
if let scrollPhase = cachedFeedScrollPhase {
|
|
184
|
-
body["feedScrollPhase"] = scrollPhase
|
|
185
|
-
} else {
|
|
186
|
-
body["feedScrollPhase"] = NSNull()
|
|
187
|
-
}
|
|
188
|
-
bridge?.emit("onOverlayFullState", body: body)
|
|
184
|
+
"activeCue": cachedActiveCueJson ?? NSNull(),
|
|
185
|
+
"feedScrollPhase": cachedFeedScrollPhase ?? NSNull(),
|
|
186
|
+
])
|
|
189
187
|
}
|
|
190
188
|
|
|
191
189
|
// MARK: - Surface Creation
|
|
@@ -226,6 +224,15 @@ import ShortKitSDK
|
|
|
226
224
|
|
|
227
225
|
// Push any pending properties now that the surface exists
|
|
228
226
|
pushInitialProperties()
|
|
227
|
+
|
|
228
|
+
// If activatePlayback() was called before the surface was ready,
|
|
229
|
+
// emit the full state now that JS can receive events.
|
|
230
|
+
if isActive {
|
|
231
|
+
DispatchQueue.main.async { [weak self] in
|
|
232
|
+
guard let self, self.isActive else { return }
|
|
233
|
+
self.emitFullState()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
229
236
|
}
|
|
230
237
|
|
|
231
238
|
// MARK: - Layout
|
|
@@ -243,17 +250,21 @@ import ShortKitSDK
|
|
|
243
250
|
// MARK: - Player Subscriptions
|
|
244
251
|
|
|
245
252
|
private func subscribeToPlayer(_ player: ShortKitPlayer) {
|
|
253
|
+
// Player state, muted, playbackRate, captionsEnabled, and activeCue
|
|
254
|
+
// are suppressed during .dragging to avoid unnecessary bridge traffic
|
|
255
|
+
// for an overlay that is scrolling off screen. emitFullState() re-syncs
|
|
256
|
+
// all cached values when the scroll settles (or on cancelled swipe).
|
|
257
|
+
|
|
246
258
|
player.playerState
|
|
247
259
|
.receive(on: DispatchQueue.main)
|
|
248
260
|
.sink { [weak self] state in
|
|
249
261
|
guard let self else { return }
|
|
250
262
|
self.cachedPlayerState = Self.playerStateString(state)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
263
|
+
guard self.isActive, !self.isDragging else { return }
|
|
264
|
+
self.bridge?.emit("onOverlayPlayerStateChanged", body: [
|
|
265
|
+
"surfaceId": self.surfaceId,
|
|
266
|
+
"playerState": self.cachedPlayerState
|
|
267
|
+
])
|
|
257
268
|
}
|
|
258
269
|
.store(in: &cancellables)
|
|
259
270
|
|
|
@@ -262,12 +273,11 @@ import ShortKitSDK
|
|
|
262
273
|
.sink { [weak self] muted in
|
|
263
274
|
guard let self else { return }
|
|
264
275
|
self.cachedIsMuted = muted
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
276
|
+
guard self.isActive, !self.isDragging else { return }
|
|
277
|
+
self.bridge?.emit("onOverlayMutedChanged", body: [
|
|
278
|
+
"surfaceId": self.surfaceId,
|
|
279
|
+
"isMuted": self.cachedIsMuted
|
|
280
|
+
])
|
|
271
281
|
}
|
|
272
282
|
.store(in: &cancellables)
|
|
273
283
|
|
|
@@ -276,12 +286,11 @@ import ShortKitSDK
|
|
|
276
286
|
.sink { [weak self] rate in
|
|
277
287
|
guard let self else { return }
|
|
278
288
|
self.cachedPlaybackRate = Double(rate)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
289
|
+
guard self.isActive, !self.isDragging else { return }
|
|
290
|
+
self.bridge?.emit("onOverlayPlaybackRateChanged", body: [
|
|
291
|
+
"surfaceId": self.surfaceId,
|
|
292
|
+
"playbackRate": self.cachedPlaybackRate
|
|
293
|
+
])
|
|
285
294
|
}
|
|
286
295
|
.store(in: &cancellables)
|
|
287
296
|
|
|
@@ -290,12 +299,11 @@ import ShortKitSDK
|
|
|
290
299
|
.sink { [weak self] enabled in
|
|
291
300
|
guard let self else { return }
|
|
292
301
|
self.cachedCaptionsEnabled = enabled
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
302
|
+
guard self.isActive, !self.isDragging else { return }
|
|
303
|
+
self.bridge?.emit("onOverlayCaptionsEnabledChanged", body: [
|
|
304
|
+
"surfaceId": self.surfaceId,
|
|
305
|
+
"captionsEnabled": self.cachedCaptionsEnabled
|
|
306
|
+
])
|
|
299
307
|
}
|
|
300
308
|
.store(in: &cancellables)
|
|
301
309
|
|
|
@@ -303,30 +311,26 @@ import ShortKitSDK
|
|
|
303
311
|
.receive(on: DispatchQueue.main)
|
|
304
312
|
.sink { [weak self] cue in
|
|
305
313
|
guard let self else { return }
|
|
314
|
+
// Pre-serialize once; reused by emitFullState() and individual emission.
|
|
306
315
|
if let cue {
|
|
307
316
|
self.cachedActiveCue = [
|
|
308
317
|
"text": cue.text,
|
|
309
318
|
"startTime": cue.startTime,
|
|
310
319
|
"endTime": cue.endTime,
|
|
311
320
|
]
|
|
312
|
-
|
|
313
|
-
self.cachedActiveCue = nil
|
|
314
|
-
}
|
|
315
|
-
if self.isActive {
|
|
316
|
-
if let cached = self.cachedActiveCue,
|
|
317
|
-
let data = try? JSONSerialization.data(withJSONObject: cached),
|
|
321
|
+
if let data = try? JSONSerialization.data(withJSONObject: self.cachedActiveCue!),
|
|
318
322
|
let json = String(data: data, encoding: .utf8) {
|
|
319
|
-
self.
|
|
320
|
-
"surfaceId": self.surfaceId,
|
|
321
|
-
"activeCue": json
|
|
322
|
-
])
|
|
323
|
-
} else {
|
|
324
|
-
self.bridge?.emit("onOverlayActiveCueChanged", body: [
|
|
325
|
-
"surfaceId": self.surfaceId,
|
|
326
|
-
"activeCue": NSNull()
|
|
327
|
-
])
|
|
323
|
+
self.cachedActiveCueJson = json
|
|
328
324
|
}
|
|
325
|
+
} else {
|
|
326
|
+
self.cachedActiveCue = nil
|
|
327
|
+
self.cachedActiveCueJson = nil
|
|
329
328
|
}
|
|
329
|
+
guard self.isActive, !self.isDragging else { return }
|
|
330
|
+
self.bridge?.emit("onOverlayActiveCueChanged", body: [
|
|
331
|
+
"surfaceId": self.surfaceId,
|
|
332
|
+
"activeCue": self.cachedActiveCueJson ?? NSNull()
|
|
333
|
+
])
|
|
330
334
|
}
|
|
331
335
|
.store(in: &cancellables)
|
|
332
336
|
|
|
@@ -334,18 +338,26 @@ import ShortKitSDK
|
|
|
334
338
|
.receive(on: DispatchQueue.main)
|
|
335
339
|
.sink { [weak self] phase in
|
|
336
340
|
guard let self else { return }
|
|
341
|
+
// Pre-serialize once; reused by emitFullState() and individual emission.
|
|
337
342
|
switch phase {
|
|
338
343
|
case .dragging(let from):
|
|
339
|
-
|
|
340
|
-
if let data = try? JSONSerialization.data(withJSONObject:
|
|
344
|
+
self.isDragging = true
|
|
345
|
+
if let data = try? JSONSerialization.data(withJSONObject: ["phase": "dragging", "fromId": from]),
|
|
341
346
|
let json = String(data: data, encoding: .utf8) {
|
|
342
347
|
self.cachedFeedScrollPhase = json
|
|
343
348
|
}
|
|
344
349
|
case .settled:
|
|
345
|
-
let
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
350
|
+
let wasDragging = self.isDragging
|
|
351
|
+
self.isDragging = false
|
|
352
|
+
self.cachedFeedScrollPhase = "{\"phase\":\"settled\"}"
|
|
353
|
+
|
|
354
|
+
// Re-sync all state that was suppressed during the drag.
|
|
355
|
+
// Handles both normal swipes (new item's activatePlayback
|
|
356
|
+
// will also fire) and cancelled swipes (same item, no
|
|
357
|
+
// activatePlayback — this is the only re-sync path).
|
|
358
|
+
if wasDragging, self.isActive {
|
|
359
|
+
self.emitFullState()
|
|
360
|
+
return // fullState includes feedScrollPhase
|
|
349
361
|
}
|
|
350
362
|
}
|
|
351
363
|
if self.isActive {
|
|
@@ -360,7 +372,7 @@ import ShortKitSDK
|
|
|
360
372
|
player.time
|
|
361
373
|
.receive(on: DispatchQueue.main)
|
|
362
374
|
.sink { [weak self] time in
|
|
363
|
-
guard let self, self.isActive else { return }
|
|
375
|
+
guard let self, self.isActive, !self.isDragging else { return }
|
|
364
376
|
self.cachedTime = (time.current, time.duration, time.buffered)
|
|
365
377
|
self.timeDirty = true
|
|
366
378
|
}
|
|
@@ -404,17 +416,15 @@ import ShortKitSDK
|
|
|
404
416
|
props["item"] = json
|
|
405
417
|
}
|
|
406
418
|
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
props["isActive"] =
|
|
410
|
-
props["playerState"] =
|
|
419
|
+
// Use current state — if activatePlayback() was called before the
|
|
420
|
+
// surface was created, isActive will already be true.
|
|
421
|
+
props["isActive"] = isActive
|
|
422
|
+
props["playerState"] = cachedPlayerState
|
|
411
423
|
props["isMuted"] = cachedIsMuted
|
|
412
424
|
props["playbackRate"] = cachedPlaybackRate
|
|
413
425
|
props["captionsEnabled"] = cachedCaptionsEnabled
|
|
414
426
|
|
|
415
|
-
if let
|
|
416
|
-
let cueData = try? JSONSerialization.data(withJSONObject: cue),
|
|
417
|
-
let cueJson = String(data: cueData, encoding: .utf8) {
|
|
427
|
+
if let cueJson = cachedActiveCueJson {
|
|
418
428
|
props["activeCue"] = cueJson
|
|
419
429
|
}
|
|
420
430
|
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import Combine
|
|
3
|
+
import ShortKitSDK
|
|
4
|
+
|
|
5
|
+
/// A UIView that conforms to `VideoCarouselOverlay` and hosts an `RCTFabricSurface`
|
|
6
|
+
/// for rendering the developer's React video carousel overlay component inside a feed cell.
|
|
7
|
+
///
|
|
8
|
+
/// Pushes `VideoCarouselItem` and active video data as surface properties, and
|
|
9
|
+
/// emits playback state (isActive, time, playerState, isMuted) via bridge events
|
|
10
|
+
/// using the same event names as `ReactOverlayHost` (routed by `surfaceId`).
|
|
11
|
+
@objc public class ReactVideoCarouselOverlayHost: UIView, @unchecked Sendable, VideoCarouselOverlay {
|
|
12
|
+
|
|
13
|
+
// MARK: - Configuration
|
|
14
|
+
|
|
15
|
+
var surfacePresenter: AnyObject?
|
|
16
|
+
weak var bridge: ShortKitBridge?
|
|
17
|
+
|
|
18
|
+
/// Module name for the RCTFabricSurface. Set by the overlay factory
|
|
19
|
+
/// based on the feed config's video carousel overlay name.
|
|
20
|
+
var videoCarouselOverlayModuleName: String = "ShortKitVideoCarouselOverlay"
|
|
21
|
+
|
|
22
|
+
// MARK: - Eager Surface Creation
|
|
23
|
+
|
|
24
|
+
func prepareSurface() {
|
|
25
|
+
createSurfaceIfNeeded()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// MARK: - State
|
|
29
|
+
|
|
30
|
+
private var surface: SKFabricSurfaceWrapper?
|
|
31
|
+
private var surfaceView: UIView?
|
|
32
|
+
private var pendingProps: [String: Any]?
|
|
33
|
+
|
|
34
|
+
/// Unique identifier for this overlay instance, used for event routing.
|
|
35
|
+
let surfaceId = UUID().uuidString
|
|
36
|
+
|
|
37
|
+
/// Cached carouselItem JSON — setProperties replaces all props, doesn't merge.
|
|
38
|
+
private var carouselItemJSON: String?
|
|
39
|
+
|
|
40
|
+
// Player state
|
|
41
|
+
private var player: ShortKitPlayer?
|
|
42
|
+
private var cancellables = Set<AnyCancellable>()
|
|
43
|
+
private var isActive = false
|
|
44
|
+
private var cachedPlayerState: String = "idle"
|
|
45
|
+
private var cachedIsMuted: Bool = true
|
|
46
|
+
private var cachedTime: (current: Double, duration: Double, buffered: Double) = (0, 0, 0)
|
|
47
|
+
private var timeCoalesceTimer: Timer?
|
|
48
|
+
private var timeDirty = false
|
|
49
|
+
|
|
50
|
+
// MARK: - Init
|
|
51
|
+
|
|
52
|
+
override init(frame: CGRect) {
|
|
53
|
+
super.init(frame: frame)
|
|
54
|
+
backgroundColor = .clear
|
|
55
|
+
isUserInteractionEnabled = true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
required init?(coder: NSCoder) {
|
|
59
|
+
fatalError("init(coder:) is not supported")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
deinit {
|
|
63
|
+
timeCoalesceTimer?.invalidate()
|
|
64
|
+
surface?.stop()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// MARK: - VideoCarouselOverlay
|
|
68
|
+
|
|
69
|
+
public func configure(with item: VideoCarouselItem) {
|
|
70
|
+
isActive = false
|
|
71
|
+
timeDirty = false
|
|
72
|
+
timeCoalesceTimer?.invalidate()
|
|
73
|
+
timeCoalesceTimer = nil
|
|
74
|
+
cachedTime = (0, 0, 0)
|
|
75
|
+
cachedPlayerState = "idle"
|
|
76
|
+
|
|
77
|
+
createSurfaceIfNeeded()
|
|
78
|
+
|
|
79
|
+
guard let data = try? JSONEncoder().encode(item),
|
|
80
|
+
let json = String(data: data, encoding: .utf8) else { return }
|
|
81
|
+
carouselItemJSON = json
|
|
82
|
+
|
|
83
|
+
var props: [String: Any] = [
|
|
84
|
+
"surfaceId": surfaceId,
|
|
85
|
+
"carouselItem": json,
|
|
86
|
+
"isActive": false,
|
|
87
|
+
"playerState": "idle",
|
|
88
|
+
"isMuted": cachedIsMuted,
|
|
89
|
+
]
|
|
90
|
+
if let firstVideo = item.videos.first,
|
|
91
|
+
let videoData = try? JSONEncoder().encode(firstVideo),
|
|
92
|
+
let videoJSON = String(data: videoData, encoding: .utf8) {
|
|
93
|
+
props["activeVideo"] = videoJSON
|
|
94
|
+
props["activeVideoIndex"] = 0
|
|
95
|
+
}
|
|
96
|
+
if let surface {
|
|
97
|
+
surface.setProperties(props)
|
|
98
|
+
} else {
|
|
99
|
+
pendingProps = props
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public func updateActiveVideo(index: Int, item: ContentItem) {
|
|
104
|
+
guard let data = try? JSONEncoder().encode(item),
|
|
105
|
+
let json = String(data: data, encoding: .utf8) else { return }
|
|
106
|
+
|
|
107
|
+
// Only emit when active — matches ReactOverlayHost pattern.
|
|
108
|
+
// During initial setup, configure() sets the first video via surface props.
|
|
109
|
+
// Before activation, the AsyncEventEmitter may not be initialized.
|
|
110
|
+
guard isActive else { return }
|
|
111
|
+
|
|
112
|
+
bridge?.emit("onVideoCarouselActiveVideoChanged", body: [
|
|
113
|
+
"surfaceId": surfaceId,
|
|
114
|
+
"activeVideo": json,
|
|
115
|
+
"activeVideoIndex": index,
|
|
116
|
+
])
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public func resetState() {
|
|
120
|
+
// Don't clear surface props — configure() will overwrite.
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public func attach(player: ShortKitPlayer) {
|
|
124
|
+
self.player = player
|
|
125
|
+
subscribeToPlayer(player)
|
|
126
|
+
createSurfaceIfNeeded()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public func activatePlayback() {
|
|
130
|
+
isActive = true
|
|
131
|
+
startTimeCoalescing()
|
|
132
|
+
|
|
133
|
+
DispatchQueue.main.async { [weak self] in
|
|
134
|
+
guard let self, self.isActive else { return }
|
|
135
|
+
self.emitFullState()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public func deactivatePlayback() {
|
|
140
|
+
isActive = false
|
|
141
|
+
timeDirty = false
|
|
142
|
+
timeCoalesceTimer?.invalidate()
|
|
143
|
+
timeCoalesceTimer = nil
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// MARK: - Player Subscriptions
|
|
147
|
+
|
|
148
|
+
private func subscribeToPlayer(_ player: ShortKitPlayer) {
|
|
149
|
+
cancellables.removeAll()
|
|
150
|
+
player.playerState
|
|
151
|
+
.receive(on: DispatchQueue.main)
|
|
152
|
+
.sink { [weak self] state in
|
|
153
|
+
guard let self else { return }
|
|
154
|
+
self.cachedPlayerState = Self.playerStateString(state)
|
|
155
|
+
if self.isActive {
|
|
156
|
+
self.bridge?.emit("onOverlayPlayerStateChanged", body: [
|
|
157
|
+
"surfaceId": self.surfaceId,
|
|
158
|
+
"playerState": self.cachedPlayerState
|
|
159
|
+
])
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
.store(in: &cancellables)
|
|
163
|
+
|
|
164
|
+
player.isMuted
|
|
165
|
+
.receive(on: DispatchQueue.main)
|
|
166
|
+
.sink { [weak self] muted in
|
|
167
|
+
guard let self else { return }
|
|
168
|
+
self.cachedIsMuted = muted
|
|
169
|
+
if self.isActive {
|
|
170
|
+
self.bridge?.emit("onOverlayMutedChanged", body: [
|
|
171
|
+
"surfaceId": self.surfaceId,
|
|
172
|
+
"isMuted": self.cachedIsMuted
|
|
173
|
+
])
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
.store(in: &cancellables)
|
|
177
|
+
|
|
178
|
+
player.time
|
|
179
|
+
.receive(on: DispatchQueue.main)
|
|
180
|
+
.sink { [weak self] time in
|
|
181
|
+
guard let self, self.isActive else { return }
|
|
182
|
+
self.cachedTime = (time.current, time.duration, time.buffered)
|
|
183
|
+
self.timeDirty = true
|
|
184
|
+
}
|
|
185
|
+
.store(in: &cancellables)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// MARK: - Time Coalescing
|
|
189
|
+
|
|
190
|
+
private func startTimeCoalescing() {
|
|
191
|
+
timeCoalesceTimer?.invalidate()
|
|
192
|
+
timeCoalesceTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
|
|
193
|
+
guard let self, self.timeDirty else { return }
|
|
194
|
+
self.timeDirty = false
|
|
195
|
+
self.bridge?.emit("onOverlayTimeUpdate", body: [
|
|
196
|
+
"surfaceId": self.surfaceId,
|
|
197
|
+
"current": self.cachedTime.current,
|
|
198
|
+
"duration": self.cachedTime.duration,
|
|
199
|
+
"buffered": self.cachedTime.buffered
|
|
200
|
+
])
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// MARK: - Full State Emission
|
|
205
|
+
|
|
206
|
+
private func emitFullState() {
|
|
207
|
+
bridge?.emit("onOverlayFullState", body: [
|
|
208
|
+
"surfaceId": surfaceId,
|
|
209
|
+
"isActive": true,
|
|
210
|
+
"playerState": cachedPlayerState,
|
|
211
|
+
"isMuted": cachedIsMuted,
|
|
212
|
+
"playbackRate": 1.0,
|
|
213
|
+
"captionsEnabled": false,
|
|
214
|
+
"activeCue": NSNull(),
|
|
215
|
+
"feedScrollPhase": NSNull(),
|
|
216
|
+
])
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// MARK: - Surface Creation
|
|
220
|
+
|
|
221
|
+
private func createSurfaceIfNeeded() {
|
|
222
|
+
guard surface == nil, let presenter = surfacePresenter else { return }
|
|
223
|
+
|
|
224
|
+
DispatchQueue.main.async { [weak self] in
|
|
225
|
+
guard let self, self.surface == nil else { return }
|
|
226
|
+
self.installSurface(presenter: presenter)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private func installSurface(presenter: AnyObject) {
|
|
231
|
+
guard let surf = SKFabricSurfaceWrapper(
|
|
232
|
+
presenter: presenter,
|
|
233
|
+
moduleName: videoCarouselOverlayModuleName,
|
|
234
|
+
initialProperties: [:]
|
|
235
|
+
) else { return }
|
|
236
|
+
surf.start()
|
|
237
|
+
|
|
238
|
+
let view = surf.view
|
|
239
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
240
|
+
addSubview(view)
|
|
241
|
+
NSLayoutConstraint.activate([
|
|
242
|
+
view.topAnchor.constraint(equalTo: topAnchor),
|
|
243
|
+
view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
244
|
+
view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
245
|
+
view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
246
|
+
])
|
|
247
|
+
surfaceView = view
|
|
248
|
+
surface = surf
|
|
249
|
+
|
|
250
|
+
// Flush any props that arrived before surface was ready
|
|
251
|
+
if let pending = pendingProps {
|
|
252
|
+
surf.setProperties(pending)
|
|
253
|
+
pendingProps = nil
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// MARK: - Layout
|
|
258
|
+
|
|
259
|
+
public override func layoutSubviews() {
|
|
260
|
+
super.layoutSubviews()
|
|
261
|
+
guard let surface else { return }
|
|
262
|
+
let size = bounds.size
|
|
263
|
+
guard size.width > 0, size.height > 0 else { return }
|
|
264
|
+
surface.setMinimumSize(size)
|
|
265
|
+
surface.setMaximumSize(size)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// MARK: - Helpers
|
|
269
|
+
|
|
270
|
+
private static func playerStateString(_ state: PlayerState) -> String {
|
|
271
|
+
switch state {
|
|
272
|
+
case .idle: return "idle"
|
|
273
|
+
case .loading: return "loading"
|
|
274
|
+
case .ready: return "ready"
|
|
275
|
+
case .playing: return "playing"
|
|
276
|
+
case .paused: return "paused"
|
|
277
|
+
case .seeking: return "seeking"
|
|
278
|
+
case .buffering: return "buffering"
|
|
279
|
+
case .ended: return "ended"
|
|
280
|
+
case .error(_): return "error"
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|