@shortkitsdk/react-native 0.1.0
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 +19 -0
- package/android/build.gradle.kts +34 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +249 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +32 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +769 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitOverlayBridge.kt +101 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitPackage.kt +40 -0
- package/app.plugin.js +1 -0
- package/ios/ShortKitBridge.swift +537 -0
- package/ios/ShortKitFeedView.swift +207 -0
- package/ios/ShortKitFeedViewManager.mm +29 -0
- package/ios/ShortKitModule.h +25 -0
- package/ios/ShortKitModule.mm +204 -0
- package/ios/ShortKitOverlayBridge.swift +91 -0
- package/ios/ShortKitReactNative-Bridging-Header.h +3 -0
- package/ios/ShortKitReactNative.podspec +19 -0
- package/package.json +50 -0
- package/plugin/build/index.d.ts +3 -0
- package/plugin/build/index.js +13 -0
- package/plugin/build/withShortKitAndroid.d.ts +8 -0
- package/plugin/build/withShortKitAndroid.js +32 -0
- package/plugin/build/withShortKitIOS.d.ts +8 -0
- package/plugin/build/withShortKitIOS.js +29 -0
- package/react-native.config.js +8 -0
- package/src/OverlayManager.tsx +87 -0
- package/src/ShortKitContext.ts +51 -0
- package/src/ShortKitFeed.tsx +203 -0
- package/src/ShortKitProvider.tsx +526 -0
- package/src/index.ts +26 -0
- package/src/serialization.ts +95 -0
- package/src/specs/NativeShortKitModule.ts +201 -0
- package/src/specs/ShortKitFeedViewNativeComponent.ts +13 -0
- package/src/types.ts +167 -0
- package/src/useShortKit.ts +20 -0
- package/src/useShortKitPlayer.ts +29 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ConfigPlugin } from '@expo/config-plugins';
|
|
2
|
+
/**
|
|
3
|
+
* iOS config plugin for ShortKit.
|
|
4
|
+
*
|
|
5
|
+
* - Sets the minimum iOS deployment target to 16.0 if lower.
|
|
6
|
+
* - No special permissions are required for video streaming playback.
|
|
7
|
+
*/
|
|
8
|
+
export declare const withShortKitIOS: ConfigPlugin;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withShortKitIOS = void 0;
|
|
4
|
+
const config_plugins_1 = require("@expo/config-plugins");
|
|
5
|
+
/**
|
|
6
|
+
* iOS config plugin for ShortKit.
|
|
7
|
+
*
|
|
8
|
+
* - Sets the minimum iOS deployment target to 16.0 if lower.
|
|
9
|
+
* - No special permissions are required for video streaming playback.
|
|
10
|
+
*/
|
|
11
|
+
const withShortKitIOS = (config) => {
|
|
12
|
+
return (0, config_plugins_1.withXcodeProject)(config, (mod) => {
|
|
13
|
+
const project = mod.modResults;
|
|
14
|
+
// Ensure minimum deployment target is iOS 16.0
|
|
15
|
+
const buildConfigs = project.pbxXCBuildConfigurationSection();
|
|
16
|
+
for (const key of Object.keys(buildConfigs)) {
|
|
17
|
+
const buildConfig = buildConfigs[key];
|
|
18
|
+
if (typeof buildConfig === 'object' &&
|
|
19
|
+
buildConfig.buildSettings?.IPHONEOS_DEPLOYMENT_TARGET) {
|
|
20
|
+
const current = parseFloat(buildConfig.buildSettings.IPHONEOS_DEPLOYMENT_TARGET);
|
|
21
|
+
if (current < 16.0) {
|
|
22
|
+
buildConfig.buildSettings.IPHONEOS_DEPLOYMENT_TARGET = '16.0';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return mod;
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
exports.withShortKitIOS = withShortKitIOS;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { useContext, useMemo } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import type { OverlayConfig, PlayerState } from './types';
|
|
4
|
+
import { ShortKitContext } from './ShortKitContext';
|
|
5
|
+
import type { ShortKitContextValue } from './ShortKitContext';
|
|
6
|
+
|
|
7
|
+
interface OverlayManagerProps {
|
|
8
|
+
overlay: OverlayConfig;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal component that renders TWO instances of the developer's custom
|
|
13
|
+
* overlay component — one for the current cell and one for the next cell.
|
|
14
|
+
*
|
|
15
|
+
* On the native side, `ShortKitFeedView` finds these views by their
|
|
16
|
+
* `nativeID` and applies scroll-tracking transforms so each overlay moves
|
|
17
|
+
* with its respective cell during swipe transitions — matching the native
|
|
18
|
+
* iOS/Android SDK behavior where each cell has its own overlay instance.
|
|
19
|
+
*
|
|
20
|
+
* The "current" overlay uses the actual provider context.
|
|
21
|
+
* The "next" overlay uses a context override where `currentItem = nextItem`
|
|
22
|
+
* with initial playback state, so it renders the upcoming video's overlay
|
|
23
|
+
* content before the user scrolls to it.
|
|
24
|
+
*/
|
|
25
|
+
export function OverlayManager({ overlay }: OverlayManagerProps) {
|
|
26
|
+
if (
|
|
27
|
+
overlay === 'none' ||
|
|
28
|
+
typeof overlay === 'string' ||
|
|
29
|
+
overlay.type !== 'custom' ||
|
|
30
|
+
!('component' in overlay)
|
|
31
|
+
) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const OverlayComponent = overlay.component;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<View style={StyleSheet.absoluteFill} nativeID="overlay-current" pointerEvents="box-none">
|
|
40
|
+
<OverlayComponent />
|
|
41
|
+
</View>
|
|
42
|
+
<View style={StyleSheet.absoluteFill} nativeID="overlay-next" pointerEvents="box-none">
|
|
43
|
+
<NextOverlayProvider>
|
|
44
|
+
<OverlayComponent />
|
|
45
|
+
</NextOverlayProvider>
|
|
46
|
+
</View>
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Wraps children with a modified ShortKitContext where `currentItem` is
|
|
53
|
+
* set to the provider's `nextItem` (the item configured for the upcoming
|
|
54
|
+
* cell). Other state reflects an "about to play" initial state so the
|
|
55
|
+
* overlay renders correctly before playback starts on that cell.
|
|
56
|
+
*/
|
|
57
|
+
function NextOverlayProvider({ children }: { children: React.ReactNode }) {
|
|
58
|
+
const context = useContext(ShortKitContext);
|
|
59
|
+
|
|
60
|
+
const nextValue: ShortKitContextValue | null = useMemo(() => {
|
|
61
|
+
if (!context) return null;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...context,
|
|
65
|
+
currentItem: context.nextItem,
|
|
66
|
+
isActive: context.nextItem != null,
|
|
67
|
+
time: {
|
|
68
|
+
current: 0,
|
|
69
|
+
duration: context.nextItem?.duration ?? 0,
|
|
70
|
+
buffered: 0,
|
|
71
|
+
},
|
|
72
|
+
playerState: 'idle' as PlayerState,
|
|
73
|
+
isTransitioning: false,
|
|
74
|
+
activeCue: null,
|
|
75
|
+
lastOverlayTap: 0,
|
|
76
|
+
lastOverlayDoubleTap: null,
|
|
77
|
+
};
|
|
78
|
+
}, [context]);
|
|
79
|
+
|
|
80
|
+
if (!nextValue) return <>{children}</>;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<ShortKitContext.Provider value={nextValue}>
|
|
84
|
+
{children}
|
|
85
|
+
</ShortKitContext.Provider>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
ContentItem,
|
|
4
|
+
PlayerTime,
|
|
5
|
+
PlayerState,
|
|
6
|
+
CaptionTrack,
|
|
7
|
+
ContentSignal,
|
|
8
|
+
OverlayConfig,
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
export interface ShortKitContextValue {
|
|
12
|
+
// Player state
|
|
13
|
+
playerState: PlayerState;
|
|
14
|
+
currentItem: ContentItem | null;
|
|
15
|
+
nextItem: ContentItem | null;
|
|
16
|
+
time: PlayerTime;
|
|
17
|
+
isMuted: boolean;
|
|
18
|
+
playbackRate: number;
|
|
19
|
+
captionsEnabled: boolean;
|
|
20
|
+
activeCaptionTrack: CaptionTrack | null;
|
|
21
|
+
activeCue: { text: string; startTime: number; endTime: number } | null;
|
|
22
|
+
prefetchedAheadCount: number;
|
|
23
|
+
isActive: boolean;
|
|
24
|
+
isTransitioning: boolean;
|
|
25
|
+
lastOverlayTap: number;
|
|
26
|
+
lastOverlayDoubleTap: { x: number; y: number; id: number } | null;
|
|
27
|
+
|
|
28
|
+
// Player commands
|
|
29
|
+
play: () => void;
|
|
30
|
+
pause: () => void;
|
|
31
|
+
seek: (seconds: number) => void;
|
|
32
|
+
seekAndPlay: (seconds: number) => void;
|
|
33
|
+
skipToNext: () => void;
|
|
34
|
+
skipToPrevious: () => void;
|
|
35
|
+
setMuted: (muted: boolean) => void;
|
|
36
|
+
setPlaybackRate: (rate: number) => void;
|
|
37
|
+
setCaptionsEnabled: (enabled: boolean) => void;
|
|
38
|
+
selectCaptionTrack: (language: string) => void;
|
|
39
|
+
sendContentSignal: (signal: ContentSignal) => void;
|
|
40
|
+
setMaxBitrate: (bitrate: number) => void;
|
|
41
|
+
|
|
42
|
+
// SDK operations
|
|
43
|
+
setUserId: (id: string) => void;
|
|
44
|
+
clearUserId: () => void;
|
|
45
|
+
|
|
46
|
+
// Internal — used by ShortKitFeed to render custom overlays
|
|
47
|
+
/** @internal */
|
|
48
|
+
_overlayConfig: OverlayConfig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const ShortKitContext = createContext<ShortKitContextValue | null>(null);
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React, { useContext, useEffect } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import type { ShortKitFeedProps } from './types';
|
|
4
|
+
import ShortKitFeedView from './specs/ShortKitFeedViewNativeComponent';
|
|
5
|
+
import NativeShortKitModule from './specs/NativeShortKitModule';
|
|
6
|
+
import { OverlayManager } from './OverlayManager';
|
|
7
|
+
import { ShortKitContext } from './ShortKitContext';
|
|
8
|
+
import { deserializeContentItem } from './serialization';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders the native ShortKit video feed and forwards feed-level events to
|
|
12
|
+
* callback props.
|
|
13
|
+
*
|
|
14
|
+
* Must be rendered inside a `<ShortKitProvider>`. The native feed view
|
|
15
|
+
* receives its configuration from the `NativeShortKitModule.initialize()` call
|
|
16
|
+
* made by the provider, so the view props here are placeholders required by
|
|
17
|
+
* the Fabric codegen contract.
|
|
18
|
+
*
|
|
19
|
+
* When the overlay config is of type `custom`, the `OverlayManager` is
|
|
20
|
+
* rendered alongside the native view to display the developer's React overlay
|
|
21
|
+
* component.
|
|
22
|
+
*/
|
|
23
|
+
export function ShortKitFeed(props: ShortKitFeedProps) {
|
|
24
|
+
const {
|
|
25
|
+
style,
|
|
26
|
+
onError,
|
|
27
|
+
onShareTapped,
|
|
28
|
+
onSurveyResponse,
|
|
29
|
+
onLoop,
|
|
30
|
+
onFeedTransition,
|
|
31
|
+
onFormatChange,
|
|
32
|
+
onArticleTapped,
|
|
33
|
+
onCommentTapped,
|
|
34
|
+
onOverlayShareTapped,
|
|
35
|
+
onSaveTapped,
|
|
36
|
+
onLikeTapped,
|
|
37
|
+
} = props;
|
|
38
|
+
|
|
39
|
+
const context = useContext(ShortKitContext);
|
|
40
|
+
if (!context) {
|
|
41
|
+
throw new Error('ShortKitFeed must be used within a ShortKitProvider');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const overlayConfig = context._overlayConfig;
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Subscribe to feed-level events and forward to callback props
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!NativeShortKitModule) return;
|
|
51
|
+
|
|
52
|
+
const subscriptions: Array<{ remove: () => void }> = [];
|
|
53
|
+
|
|
54
|
+
if (onError) {
|
|
55
|
+
subscriptions.push(
|
|
56
|
+
NativeShortKitModule.onError((event) => {
|
|
57
|
+
onError({ code: event.code, message: event.message });
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (onShareTapped) {
|
|
63
|
+
subscriptions.push(
|
|
64
|
+
NativeShortKitModule.onShareTapped((event) => {
|
|
65
|
+
const item = deserializeContentItem(event.item);
|
|
66
|
+
if (item) onShareTapped(item);
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (onSurveyResponse) {
|
|
72
|
+
subscriptions.push(
|
|
73
|
+
NativeShortKitModule.onSurveyResponse((event) => {
|
|
74
|
+
onSurveyResponse(event.surveyId, {
|
|
75
|
+
id: event.optionId,
|
|
76
|
+
text: event.optionText,
|
|
77
|
+
});
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (onLoop) {
|
|
83
|
+
subscriptions.push(
|
|
84
|
+
NativeShortKitModule.onDidLoop((event) => {
|
|
85
|
+
onLoop({ contentId: event.contentId, loopCount: event.loopCount });
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (onFeedTransition) {
|
|
91
|
+
subscriptions.push(
|
|
92
|
+
NativeShortKitModule.onFeedTransition((event) => {
|
|
93
|
+
onFeedTransition({
|
|
94
|
+
phase: event.phase as 'began' | 'ended',
|
|
95
|
+
from: deserializeContentItem(event.fromItem ?? null),
|
|
96
|
+
to: deserializeContentItem(event.toItem ?? null),
|
|
97
|
+
direction: event.direction as 'forward' | 'backward',
|
|
98
|
+
});
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (onFormatChange) {
|
|
104
|
+
subscriptions.push(
|
|
105
|
+
NativeShortKitModule.onFormatChange((event) => {
|
|
106
|
+
onFormatChange({
|
|
107
|
+
contentId: event.contentId,
|
|
108
|
+
fromBitrate: event.fromBitrate,
|
|
109
|
+
toBitrate: event.toBitrate,
|
|
110
|
+
fromResolution: event.fromResolution,
|
|
111
|
+
toResolution: event.toResolution,
|
|
112
|
+
});
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (onArticleTapped) {
|
|
118
|
+
subscriptions.push(
|
|
119
|
+
NativeShortKitModule.onArticleTapped((event) => {
|
|
120
|
+
const item = deserializeContentItem(event.item);
|
|
121
|
+
if (item) onArticleTapped(item);
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (onCommentTapped) {
|
|
127
|
+
subscriptions.push(
|
|
128
|
+
NativeShortKitModule.onCommentTapped((event) => {
|
|
129
|
+
const item = deserializeContentItem(event.item);
|
|
130
|
+
if (item) onCommentTapped(item);
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (onOverlayShareTapped) {
|
|
136
|
+
subscriptions.push(
|
|
137
|
+
NativeShortKitModule.onOverlayShareTapped((event) => {
|
|
138
|
+
const item = deserializeContentItem(event.item);
|
|
139
|
+
if (item) onOverlayShareTapped(item);
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (onSaveTapped) {
|
|
145
|
+
subscriptions.push(
|
|
146
|
+
NativeShortKitModule.onSaveTapped((event) => {
|
|
147
|
+
const item = deserializeContentItem(event.item);
|
|
148
|
+
if (item) onSaveTapped(item);
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (onLikeTapped) {
|
|
154
|
+
subscriptions.push(
|
|
155
|
+
NativeShortKitModule.onLikeTapped((event) => {
|
|
156
|
+
const item = deserializeContentItem(event.item);
|
|
157
|
+
if (item) onLikeTapped(item);
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return () => {
|
|
163
|
+
for (const sub of subscriptions) {
|
|
164
|
+
sub.remove();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}, [
|
|
168
|
+
onError,
|
|
169
|
+
onShareTapped,
|
|
170
|
+
onSurveyResponse,
|
|
171
|
+
onLoop,
|
|
172
|
+
onFeedTransition,
|
|
173
|
+
onFormatChange,
|
|
174
|
+
onArticleTapped,
|
|
175
|
+
onCommentTapped,
|
|
176
|
+
onOverlayShareTapped,
|
|
177
|
+
onSaveTapped,
|
|
178
|
+
onLikeTapped,
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Render
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
return (
|
|
185
|
+
<View style={[feedStyles.container, style]}>
|
|
186
|
+
<ShortKitFeedView
|
|
187
|
+
style={feedStyles.feed}
|
|
188
|
+
config="{}"
|
|
189
|
+
/>
|
|
190
|
+
<OverlayManager overlay={overlayConfig} />
|
|
191
|
+
</View>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const feedStyles = StyleSheet.create({
|
|
196
|
+
container: {
|
|
197
|
+
flex: 1,
|
|
198
|
+
overflow: 'hidden',
|
|
199
|
+
},
|
|
200
|
+
feed: {
|
|
201
|
+
flex: 1,
|
|
202
|
+
},
|
|
203
|
+
});
|