@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
|
@@ -5,8 +5,8 @@ import ShortKitSDK
|
|
|
5
5
|
/// for rendering the developer's React carousel overlay component inside a feed cell.
|
|
6
6
|
///
|
|
7
7
|
/// Unlike ReactOverlayHost, this does not subscribe to player state (carousels
|
|
8
|
-
/// are image-based). It
|
|
9
|
-
///
|
|
8
|
+
/// are image-based). It delivers `isActive` and `activeImageIndex` via bridge
|
|
9
|
+
/// events (routed by surfaceId) to avoid Fabric remounts.
|
|
10
10
|
@objc public class ReactCarouselOverlayHost: UIView, @unchecked Sendable, CarouselOverlay {
|
|
11
11
|
|
|
12
12
|
// MARK: - Configuration
|
|
@@ -20,9 +20,6 @@ import ShortKitSDK
|
|
|
20
20
|
|
|
21
21
|
// MARK: - Eager Surface Creation
|
|
22
22
|
|
|
23
|
-
/// Eagerly create the RN surface so it's ready before the first configure().
|
|
24
|
-
/// Called by the overlay factory right after setting surfacePresenter and moduleName.
|
|
25
|
-
/// Mirrors ReactOverlayHost.attach(player:) which does the same for video overlays.
|
|
26
23
|
func prepareSurface() {
|
|
27
24
|
createSurfaceIfNeeded()
|
|
28
25
|
}
|
|
@@ -37,6 +34,11 @@ import ShortKitSDK
|
|
|
37
34
|
private var surface: SKFabricSurfaceWrapper?
|
|
38
35
|
private var surfaceView: UIView?
|
|
39
36
|
private var pendingProps: [String: Any]?
|
|
37
|
+
private var isActive = false
|
|
38
|
+
private var activeImageIndex = 0
|
|
39
|
+
|
|
40
|
+
/// Unique identifier for this overlay instance, used for event routing.
|
|
41
|
+
let surfaceId = UUID().uuidString
|
|
40
42
|
|
|
41
43
|
// MARK: - Init
|
|
42
44
|
|
|
@@ -57,11 +59,11 @@ import ShortKitSDK
|
|
|
57
59
|
// MARK: - CarouselOverlay
|
|
58
60
|
|
|
59
61
|
public func configure(with item: ImageCarouselItem) {
|
|
62
|
+
isActive = false
|
|
63
|
+
activeImageIndex = 0
|
|
60
64
|
createSurfaceIfNeeded()
|
|
61
65
|
|
|
62
66
|
// Replace remote URLs with local file URLs for any natively-cached images.
|
|
63
|
-
// The SDK's PrefetchManager downloads images into NSCache<NSString, UIImage>
|
|
64
|
-
// which RN can't access. Writing to temp files lets RN load instantly from disk.
|
|
65
67
|
var modifiedItem = item
|
|
66
68
|
if let cachedImage {
|
|
67
69
|
var localImages: [CarouselImage] = []
|
|
@@ -85,10 +87,12 @@ import ShortKitSDK
|
|
|
85
87
|
)
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
// Build props — push now if surface exists, otherwise store for later
|
|
89
90
|
if let data = try? JSONEncoder().encode(modifiedItem),
|
|
90
91
|
let json = String(data: data, encoding: .utf8) {
|
|
91
|
-
let props: [String: Any] = [
|
|
92
|
+
let props: [String: Any] = [
|
|
93
|
+
"surfaceId": surfaceId,
|
|
94
|
+
"item": json,
|
|
95
|
+
]
|
|
92
96
|
if let surface {
|
|
93
97
|
surface.setProperties(props)
|
|
94
98
|
} else {
|
|
@@ -97,14 +101,35 @@ import ShortKitSDK
|
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
public func activatePlayback() {
|
|
105
|
+
isActive = true
|
|
106
|
+
bridge?.emit("onOverlayFullState", body: [
|
|
107
|
+
"surfaceId": surfaceId,
|
|
108
|
+
"isActive": true,
|
|
109
|
+
// Include required fields for the full state event
|
|
110
|
+
"playerState": "idle",
|
|
111
|
+
"isMuted": true,
|
|
112
|
+
"playbackRate": 1.0,
|
|
113
|
+
"captionsEnabled": false,
|
|
114
|
+
"activeCue": NSNull(),
|
|
115
|
+
"feedScrollPhase": NSNull(),
|
|
116
|
+
])
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public func updateActiveImage(index: Int) {
|
|
120
|
+
activeImageIndex = index
|
|
121
|
+
bridge?.emit("onCarouselActiveImageChanged", body: [
|
|
122
|
+
"surfaceId": surfaceId,
|
|
123
|
+
"activeImageIndex": index,
|
|
124
|
+
])
|
|
125
|
+
}
|
|
126
|
+
|
|
100
127
|
/// Write a UIImage to the temp directory, returning a file:// URL string.
|
|
101
|
-
/// Uses a hash of the remote URL as the filename to avoid duplicates.
|
|
102
128
|
private func writeTempImage(_ image: UIImage, for remoteURL: String) -> String? {
|
|
103
129
|
let hash = remoteURL.hash
|
|
104
130
|
let fileName = "sk-carousel-\(abs(hash)).jpg"
|
|
105
131
|
let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
|
|
106
132
|
|
|
107
|
-
// Skip if already written
|
|
108
133
|
if FileManager.default.fileExists(atPath: fileURL.path) {
|
|
109
134
|
return fileURL.absoluteString
|
|
110
135
|
}
|
|
@@ -119,10 +144,7 @@ import ShortKitSDK
|
|
|
119
144
|
}
|
|
120
145
|
|
|
121
146
|
public func resetState() {
|
|
122
|
-
// Don't clear surface props —
|
|
123
|
-
// configure() will overwrite with new props directly, letting React do an
|
|
124
|
-
// in-place re-render (old→new) instead of an unmount+remount (old→empty→new).
|
|
125
|
-
// The overlay's synchronous reset pattern handles state transitions.
|
|
147
|
+
// Don't clear surface props — configure() will overwrite.
|
|
126
148
|
}
|
|
127
149
|
|
|
128
150
|
// MARK: - Surface Creation
|
|
@@ -130,7 +152,6 @@ import ShortKitSDK
|
|
|
130
152
|
private func createSurfaceIfNeeded() {
|
|
131
153
|
guard surface == nil, let presenter = surfacePresenter else { return }
|
|
132
154
|
|
|
133
|
-
// Must always dispatch to main queue — see ReactOverlayHost for details.
|
|
134
155
|
DispatchQueue.main.async { [weak self] in
|
|
135
156
|
guard let self, self.surface == nil else { return }
|
|
136
157
|
self.installSurface(presenter: presenter)
|
|
@@ -157,7 +178,6 @@ import ShortKitSDK
|
|
|
157
178
|
surfaceView = view
|
|
158
179
|
surface = surf
|
|
159
180
|
|
|
160
|
-
// Flush any props that arrived before surface was ready
|
|
161
181
|
if let pending = pendingProps {
|
|
162
182
|
surf.setProperties(pending)
|
|
163
183
|
pendingProps = nil
|
|
@@ -126,6 +126,7 @@ import ShortKitSDK
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
public func configure(with item: ContentItem) {
|
|
129
|
+
let isSameItem = item.id == currentItem?.id
|
|
129
130
|
currentItem = item
|
|
130
131
|
isActive = false
|
|
131
132
|
timeDirty = false
|
|
@@ -140,10 +141,12 @@ import ShortKitSDK
|
|
|
140
141
|
|
|
141
142
|
createSurfaceIfNeeded()
|
|
142
143
|
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
144
|
+
// Only push properties if the surface already exists.
|
|
145
|
+
// If the surface is still being created asynchronously,
|
|
146
|
+
// installSurface() will call pushInitialProperties() when ready.
|
|
147
|
+
if surface != nil && !isSameItem {
|
|
148
|
+
pushInitialProperties()
|
|
149
|
+
}
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
public func activatePlayback() {
|
|
@@ -226,6 +229,15 @@ import ShortKitSDK
|
|
|
226
229
|
|
|
227
230
|
// Push any pending properties now that the surface exists
|
|
228
231
|
pushInitialProperties()
|
|
232
|
+
|
|
233
|
+
// If activatePlayback() was called before the surface was ready,
|
|
234
|
+
// emit the full state now that JS can receive events.
|
|
235
|
+
if isActive {
|
|
236
|
+
DispatchQueue.main.async { [weak self] in
|
|
237
|
+
guard let self, self.isActive else { return }
|
|
238
|
+
self.emitFullState()
|
|
239
|
+
}
|
|
240
|
+
}
|
|
229
241
|
}
|
|
230
242
|
|
|
231
243
|
// MARK: - Layout
|
|
@@ -404,10 +416,10 @@ 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
|
|
@@ -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
|
+
}
|
package/ios/ShortKitBridge.swift
CHANGED
|
@@ -651,6 +651,7 @@ import ShortKitSDK
|
|
|
651
651
|
let feedSource: FeedSource = feedSourceStr == "custom" ? .custom : .algorithmic
|
|
652
652
|
|
|
653
653
|
let carouselOverlay = parseCarouselOverlay(obj["carouselOverlay"] as? String)
|
|
654
|
+
let videoCarouselOverlay = parseVideoCarouselOverlay(obj["videoCarouselOverlay"] as? String)
|
|
654
655
|
|
|
655
656
|
let filter = parseFeedFilter(obj["filter"] as? String)
|
|
656
657
|
|
|
@@ -662,6 +663,7 @@ import ShortKitSDK
|
|
|
662
663
|
scrollAxis: scrollAxis,
|
|
663
664
|
videoOverlay: videoOverlay,
|
|
664
665
|
carouselOverlay: carouselOverlay,
|
|
666
|
+
videoCarouselOverlay: videoCarouselOverlay,
|
|
665
667
|
surveyOverlay: .none,
|
|
666
668
|
adOverlay: .none,
|
|
667
669
|
muteOnStart: muteOnStart,
|
|
@@ -740,6 +742,39 @@ import ShortKitSDK
|
|
|
740
742
|
return .none
|
|
741
743
|
}
|
|
742
744
|
|
|
745
|
+
/// Parse a double-stringified video carousel overlay JSON into a `VideoCarouselOverlayMode`.
|
|
746
|
+
///
|
|
747
|
+
/// Examples:
|
|
748
|
+
/// - `"\"none\""` -> `.none`
|
|
749
|
+
/// - `"{\"type\":\"custom\",\"name\":\"news\"}"` -> `.custom { ReactVideoCarouselOverlayHost() }`
|
|
750
|
+
private static func parseVideoCarouselOverlay(_ json: String?) -> VideoCarouselOverlayMode {
|
|
751
|
+
guard let json,
|
|
752
|
+
let data = json.data(using: .utf8),
|
|
753
|
+
let parsed = try? JSONSerialization.jsonObject(with: data) else {
|
|
754
|
+
return .none
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if let str = parsed as? String, str == "none" {
|
|
758
|
+
return .none
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if let obj = parsed as? [String: Any],
|
|
762
|
+
let type = obj["type"] as? String,
|
|
763
|
+
type == "custom" {
|
|
764
|
+
let name = obj["name"] as? String ?? "Default"
|
|
765
|
+
return .custom { @Sendable in
|
|
766
|
+
let host = ReactVideoCarouselOverlayHost()
|
|
767
|
+
host.surfacePresenter = ShortKitBridge.shared?.surfacePresenter
|
|
768
|
+
host.bridge = ShortKitBridge.shared
|
|
769
|
+
host.videoCarouselOverlayModuleName = "ShortKitVideoCarouselOverlay_\(name)"
|
|
770
|
+
host.prepareSurface()
|
|
771
|
+
return host
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return .none
|
|
776
|
+
}
|
|
777
|
+
|
|
743
778
|
/// Parse a double-stringified feedHeight JSON.
|
|
744
779
|
/// e.g. `"{\"type\":\"fullscreen\"}"` or `"{\"type\":\"percentage\",\"value\":0.8}"`
|
|
745
780
|
private static func parseFeedHeight(_ json: String?) -> FeedHeight {
|
|
@@ -800,6 +835,13 @@ import ShortKitSDK
|
|
|
800
835
|
continue
|
|
801
836
|
}
|
|
802
837
|
result.append(.imageCarousel(carouselItem))
|
|
838
|
+
case "videoCarousel":
|
|
839
|
+
guard let itemData = obj["item"] as? [String: Any],
|
|
840
|
+
let itemJSON = try? JSONSerialization.data(withJSONObject: itemData),
|
|
841
|
+
let carouselItem = try? JSONDecoder().decode(VideoCarouselItem.self, from: itemJSON) else {
|
|
842
|
+
continue
|
|
843
|
+
}
|
|
844
|
+
result.append(.videoCarousel(carouselItem))
|
|
803
845
|
default:
|
|
804
846
|
continue
|
|
805
847
|
}
|
package/ios/ShortKitModule.mm
CHANGED
|
@@ -83,6 +83,8 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
83
83
|
@"onOverlayFeedScrollPhaseChanged",
|
|
84
84
|
@"onOverlayTimeUpdate",
|
|
85
85
|
@"onOverlayFullState",
|
|
86
|
+
@"onCarouselActiveImageChanged",
|
|
87
|
+
@"onVideoCarouselActiveVideoChanged",
|
|
86
88
|
];
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -106,7 +108,6 @@ RCT_EXPORT_MODULE(ShortKitModule)
|
|
|
106
108
|
|
|
107
109
|
- (void)emitEvent:(NSString *)name body:(NSDictionary *)body {
|
|
108
110
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
109
|
-
// New Architecture: use the codegen EventEmitterCallback directly
|
|
110
111
|
if (_eventEmitterCallback) {
|
|
111
112
|
_eventEmitterCallback(std::string([name UTF8String]), body);
|
|
112
113
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<key>BinaryPath</key>
|
|
9
9
|
<string>ShortKitSDK.framework/ShortKitSDK</string>
|
|
10
10
|
<key>LibraryIdentifier</key>
|
|
11
|
-
<string>ios-arm64
|
|
11
|
+
<string>ios-arm64</string>
|
|
12
12
|
<key>LibraryPath</key>
|
|
13
13
|
<string>ShortKitSDK.framework</string>
|
|
14
14
|
<key>SupportedArchitectures</key>
|
|
@@ -17,14 +17,12 @@
|
|
|
17
17
|
</array>
|
|
18
18
|
<key>SupportedPlatform</key>
|
|
19
19
|
<string>ios</string>
|
|
20
|
-
<key>SupportedPlatformVariant</key>
|
|
21
|
-
<string>simulator</string>
|
|
22
20
|
</dict>
|
|
23
21
|
<dict>
|
|
24
22
|
<key>BinaryPath</key>
|
|
25
23
|
<string>ShortKitSDK.framework/ShortKitSDK</string>
|
|
26
24
|
<key>LibraryIdentifier</key>
|
|
27
|
-
<string>ios-arm64</string>
|
|
25
|
+
<string>ios-arm64-simulator</string>
|
|
28
26
|
<key>LibraryPath</key>
|
|
29
27
|
<string>ShortKitSDK.framework</string>
|
|
30
28
|
<key>SupportedArchitectures</key>
|
|
@@ -33,6 +31,8 @@
|
|
|
33
31
|
</array>
|
|
34
32
|
<key>SupportedPlatform</key>
|
|
35
33
|
<string>ios</string>
|
|
34
|
+
<key>SupportedPlatformVariant</key>
|
|
35
|
+
<string>simulator</string>
|
|
36
36
|
</dict>
|
|
37
37
|
</array>
|
|
38
38
|
<key>CFBundlePackageType</key>
|