@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,526 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
|
|
2
|
+
import { AppState } from 'react-native';
|
|
3
|
+
import type { AppStateStatus } from 'react-native';
|
|
4
|
+
import { ShortKitContext } from './ShortKitContext';
|
|
5
|
+
import type { ShortKitContextValue } from './ShortKitContext';
|
|
6
|
+
import type {
|
|
7
|
+
ShortKitProviderProps,
|
|
8
|
+
ContentItem,
|
|
9
|
+
PlayerTime,
|
|
10
|
+
PlayerState,
|
|
11
|
+
CaptionTrack,
|
|
12
|
+
ContentSignal,
|
|
13
|
+
} from './types';
|
|
14
|
+
import {
|
|
15
|
+
serializeFeedConfigForModule,
|
|
16
|
+
deserializePlayerState,
|
|
17
|
+
deserializeContentItem,
|
|
18
|
+
deserializePlayerTime,
|
|
19
|
+
} from './serialization';
|
|
20
|
+
import NativeShortKitModule from './specs/NativeShortKitModule';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// State & Reducer
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
interface State {
|
|
27
|
+
playerState: PlayerState;
|
|
28
|
+
currentItem: ContentItem | null;
|
|
29
|
+
nextItem: ContentItem | null;
|
|
30
|
+
time: PlayerTime;
|
|
31
|
+
isMuted: boolean;
|
|
32
|
+
playbackRate: number;
|
|
33
|
+
captionsEnabled: boolean;
|
|
34
|
+
activeCaptionTrack: CaptionTrack | null;
|
|
35
|
+
activeCue: { text: string; startTime: number; endTime: number } | null;
|
|
36
|
+
prefetchedAheadCount: number;
|
|
37
|
+
isActive: boolean;
|
|
38
|
+
isTransitioning: boolean;
|
|
39
|
+
lastOverlayTap: number;
|
|
40
|
+
lastOverlayDoubleTap: { x: number; y: number; id: number } | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const initialState: State = {
|
|
44
|
+
playerState: 'idle',
|
|
45
|
+
currentItem: null,
|
|
46
|
+
nextItem: null,
|
|
47
|
+
time: { current: 0, duration: 0, buffered: 0 },
|
|
48
|
+
isMuted: true,
|
|
49
|
+
playbackRate: 1.0,
|
|
50
|
+
captionsEnabled: false,
|
|
51
|
+
activeCaptionTrack: null,
|
|
52
|
+
activeCue: null,
|
|
53
|
+
prefetchedAheadCount: 0,
|
|
54
|
+
isActive: false,
|
|
55
|
+
isTransitioning: false,
|
|
56
|
+
lastOverlayTap: 0,
|
|
57
|
+
lastOverlayDoubleTap: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type Action =
|
|
61
|
+
| { type: 'PLAYER_STATE'; payload: PlayerState }
|
|
62
|
+
| { type: 'CURRENT_ITEM'; payload: ContentItem | null }
|
|
63
|
+
| { type: 'TIME'; payload: PlayerTime }
|
|
64
|
+
| { type: 'MUTED'; payload: boolean }
|
|
65
|
+
| { type: 'PLAYBACK_RATE'; payload: number }
|
|
66
|
+
| { type: 'CAPTIONS_ENABLED'; payload: boolean }
|
|
67
|
+
| { type: 'CAPTION_TRACK'; payload: CaptionTrack | null }
|
|
68
|
+
| { type: 'CUE'; payload: { text: string; startTime: number; endTime: number } | null }
|
|
69
|
+
| { type: 'PREFETCH_COUNT'; payload: number }
|
|
70
|
+
| { type: 'OVERLAY_CONFIGURE'; payload: ContentItem }
|
|
71
|
+
| { type: 'OVERLAY_ACTIVATE' }
|
|
72
|
+
| { type: 'OVERLAY_RESET' }
|
|
73
|
+
| { type: 'OVERLAY_FADE_OUT' }
|
|
74
|
+
| { type: 'OVERLAY_RESTORE' }
|
|
75
|
+
| { type: 'OVERLAY_TAP' }
|
|
76
|
+
| { type: 'OVERLAY_DOUBLE_TAP'; payload: { x: number; y: number } };
|
|
77
|
+
|
|
78
|
+
function reducer(state: State, action: Action): State {
|
|
79
|
+
switch (action.type) {
|
|
80
|
+
case 'PLAYER_STATE': {
|
|
81
|
+
// Only allow isActive to transition TO true from player state.
|
|
82
|
+
// Once active, the overlay stays mounted — hiding during transient
|
|
83
|
+
// states like "loading" between videos would cause a visible flash.
|
|
84
|
+
const isPlaybackActive =
|
|
85
|
+
action.payload === 'playing' ||
|
|
86
|
+
action.payload === 'paused' ||
|
|
87
|
+
action.payload === 'buffering' ||
|
|
88
|
+
action.payload === 'seeking';
|
|
89
|
+
return {
|
|
90
|
+
...state,
|
|
91
|
+
playerState: action.payload,
|
|
92
|
+
isActive: state.isActive || isPlaybackActive,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
case 'CURRENT_ITEM':
|
|
96
|
+
return { ...state, currentItem: action.payload };
|
|
97
|
+
case 'TIME':
|
|
98
|
+
return { ...state, time: action.payload };
|
|
99
|
+
case 'MUTED':
|
|
100
|
+
return { ...state, isMuted: action.payload };
|
|
101
|
+
case 'PLAYBACK_RATE':
|
|
102
|
+
return { ...state, playbackRate: action.payload };
|
|
103
|
+
case 'CAPTIONS_ENABLED':
|
|
104
|
+
return { ...state, captionsEnabled: action.payload };
|
|
105
|
+
case 'CAPTION_TRACK':
|
|
106
|
+
return { ...state, activeCaptionTrack: action.payload };
|
|
107
|
+
case 'CUE':
|
|
108
|
+
return { ...state, activeCue: action.payload };
|
|
109
|
+
case 'PREFETCH_COUNT':
|
|
110
|
+
return { ...state, prefetchedAheadCount: action.payload };
|
|
111
|
+
case 'OVERLAY_CONFIGURE':
|
|
112
|
+
return { ...state, nextItem: action.payload };
|
|
113
|
+
case 'OVERLAY_ACTIVATE':
|
|
114
|
+
return { ...state, currentItem: state.nextItem, isActive: true };
|
|
115
|
+
case 'OVERLAY_RESET':
|
|
116
|
+
// Don't set isActive = false — the overlay stays mounted during
|
|
117
|
+
// transitions. In the native SDK each cell has its own overlay
|
|
118
|
+
// instance, so there's no gap. We replicate this by keeping the
|
|
119
|
+
// single JS overlay mounted and updating its content on activate.
|
|
120
|
+
return { ...state, isTransitioning: false };
|
|
121
|
+
case 'OVERLAY_FADE_OUT':
|
|
122
|
+
return { ...state, isTransitioning: true };
|
|
123
|
+
case 'OVERLAY_RESTORE':
|
|
124
|
+
return { ...state, isTransitioning: false };
|
|
125
|
+
case 'OVERLAY_TAP':
|
|
126
|
+
return { ...state, lastOverlayTap: state.lastOverlayTap + 1 };
|
|
127
|
+
case 'OVERLAY_DOUBLE_TAP':
|
|
128
|
+
return {
|
|
129
|
+
...state,
|
|
130
|
+
lastOverlayDoubleTap: {
|
|
131
|
+
x: action.payload.x,
|
|
132
|
+
y: action.payload.y,
|
|
133
|
+
id: (state.lastOverlayDoubleTap?.id ?? 0) + 1,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
default:
|
|
137
|
+
return state;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Provider Component
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
export function ShortKitProvider({
|
|
146
|
+
apiKey,
|
|
147
|
+
config,
|
|
148
|
+
userId,
|
|
149
|
+
clientAppName,
|
|
150
|
+
clientAppVersion,
|
|
151
|
+
customDimensions,
|
|
152
|
+
children,
|
|
153
|
+
}: ShortKitProviderProps): React.JSX.Element {
|
|
154
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
155
|
+
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
|
|
156
|
+
|
|
157
|
+
// -----------------------------------------------------------------------
|
|
158
|
+
// Initialize / Release
|
|
159
|
+
// -----------------------------------------------------------------------
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!NativeShortKitModule) return;
|
|
162
|
+
|
|
163
|
+
const serializedConfig = serializeFeedConfigForModule(config);
|
|
164
|
+
const serializedDimensions = customDimensions
|
|
165
|
+
? JSON.stringify(customDimensions)
|
|
166
|
+
: undefined;
|
|
167
|
+
|
|
168
|
+
NativeShortKitModule.initialize(
|
|
169
|
+
apiKey,
|
|
170
|
+
serializedConfig,
|
|
171
|
+
clientAppName,
|
|
172
|
+
clientAppVersion,
|
|
173
|
+
serializedDimensions,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
NativeShortKitModule.destroy();
|
|
178
|
+
};
|
|
179
|
+
// Intentionally only run on mount/unmount. Config changes require a
|
|
180
|
+
// remount (via a `key` prop on the provider) to take effect.
|
|
181
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
182
|
+
}, []);
|
|
183
|
+
|
|
184
|
+
// -----------------------------------------------------------------------
|
|
185
|
+
// User ID sync
|
|
186
|
+
// -----------------------------------------------------------------------
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!NativeShortKitModule) return;
|
|
189
|
+
if (userId) {
|
|
190
|
+
NativeShortKitModule.setUserId(userId);
|
|
191
|
+
} else {
|
|
192
|
+
NativeShortKitModule.clearUserId();
|
|
193
|
+
}
|
|
194
|
+
}, [userId]);
|
|
195
|
+
|
|
196
|
+
// -----------------------------------------------------------------------
|
|
197
|
+
// AppState handling (background / foreground)
|
|
198
|
+
// -----------------------------------------------------------------------
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (!NativeShortKitModule) return;
|
|
201
|
+
|
|
202
|
+
const subscription = AppState.addEventListener(
|
|
203
|
+
'change',
|
|
204
|
+
(nextState: AppStateStatus) => {
|
|
205
|
+
if (
|
|
206
|
+
appStateRef.current.match(/active/) &&
|
|
207
|
+
nextState === 'background'
|
|
208
|
+
) {
|
|
209
|
+
NativeShortKitModule.onPause();
|
|
210
|
+
} else if (
|
|
211
|
+
appStateRef.current.match(/background/) &&
|
|
212
|
+
nextState === 'active'
|
|
213
|
+
) {
|
|
214
|
+
NativeShortKitModule.onResume();
|
|
215
|
+
}
|
|
216
|
+
appStateRef.current = nextState;
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return () => {
|
|
221
|
+
subscription.remove();
|
|
222
|
+
};
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
// -----------------------------------------------------------------------
|
|
226
|
+
// Event subscriptions
|
|
227
|
+
// -----------------------------------------------------------------------
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (!NativeShortKitModule) return;
|
|
230
|
+
|
|
231
|
+
const subscriptions: Array<{ remove: () => void }> = [];
|
|
232
|
+
|
|
233
|
+
// Player state
|
|
234
|
+
subscriptions.push(
|
|
235
|
+
NativeShortKitModule.onPlayerStateChanged((event) => {
|
|
236
|
+
dispatch({
|
|
237
|
+
type: 'PLAYER_STATE',
|
|
238
|
+
payload: deserializePlayerState(event),
|
|
239
|
+
});
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Current item
|
|
244
|
+
subscriptions.push(
|
|
245
|
+
NativeShortKitModule.onCurrentItemChanged((event) => {
|
|
246
|
+
const item: ContentItem = {
|
|
247
|
+
id: event.id,
|
|
248
|
+
title: event.title,
|
|
249
|
+
description: event.description,
|
|
250
|
+
duration: event.duration,
|
|
251
|
+
streamingUrl: event.streamingUrl,
|
|
252
|
+
thumbnailUrl: event.thumbnailUrl,
|
|
253
|
+
captionTracks: event.captionTracks
|
|
254
|
+
? JSON.parse(event.captionTracks)
|
|
255
|
+
: [],
|
|
256
|
+
customMetadata: event.customMetadata
|
|
257
|
+
? JSON.parse(event.customMetadata)
|
|
258
|
+
: undefined,
|
|
259
|
+
author: event.author,
|
|
260
|
+
articleUrl: event.articleUrl,
|
|
261
|
+
commentCount: event.commentCount,
|
|
262
|
+
};
|
|
263
|
+
dispatch({ type: 'CURRENT_ITEM', payload: item });
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Time updates
|
|
268
|
+
subscriptions.push(
|
|
269
|
+
NativeShortKitModule.onTimeUpdate((event) => {
|
|
270
|
+
dispatch({ type: 'TIME', payload: deserializePlayerTime(event) });
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Muted
|
|
275
|
+
subscriptions.push(
|
|
276
|
+
NativeShortKitModule.onMutedChanged((event) => {
|
|
277
|
+
dispatch({ type: 'MUTED', payload: event.isMuted });
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Playback rate
|
|
282
|
+
subscriptions.push(
|
|
283
|
+
NativeShortKitModule.onPlaybackRateChanged((event) => {
|
|
284
|
+
dispatch({ type: 'PLAYBACK_RATE', payload: event.rate });
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Captions enabled
|
|
289
|
+
subscriptions.push(
|
|
290
|
+
NativeShortKitModule.onCaptionsEnabledChanged((event) => {
|
|
291
|
+
dispatch({ type: 'CAPTIONS_ENABLED', payload: event.enabled });
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Active caption track
|
|
296
|
+
subscriptions.push(
|
|
297
|
+
NativeShortKitModule.onActiveCaptionTrackChanged((event) => {
|
|
298
|
+
dispatch({
|
|
299
|
+
type: 'CAPTION_TRACK',
|
|
300
|
+
payload: {
|
|
301
|
+
language: event.language,
|
|
302
|
+
label: event.label,
|
|
303
|
+
sourceUrl: event.sourceUrl,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Active cue
|
|
310
|
+
subscriptions.push(
|
|
311
|
+
NativeShortKitModule.onActiveCueChanged((event) => {
|
|
312
|
+
dispatch({
|
|
313
|
+
type: 'CUE',
|
|
314
|
+
payload: {
|
|
315
|
+
text: event.text,
|
|
316
|
+
startTime: event.startTime,
|
|
317
|
+
endTime: event.endTime,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
}),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Prefetch count
|
|
324
|
+
subscriptions.push(
|
|
325
|
+
NativeShortKitModule.onPrefetchedAheadCountChanged((event) => {
|
|
326
|
+
dispatch({ type: 'PREFETCH_COUNT', payload: event.count });
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// Overlay lifecycle events
|
|
331
|
+
subscriptions.push(
|
|
332
|
+
NativeShortKitModule.onOverlayConfigure((event) => {
|
|
333
|
+
const item = deserializeContentItem(event.item);
|
|
334
|
+
if (item) {
|
|
335
|
+
dispatch({ type: 'OVERLAY_CONFIGURE', payload: item });
|
|
336
|
+
}
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
subscriptions.push(
|
|
341
|
+
NativeShortKitModule.onOverlayActivate((_event) => {
|
|
342
|
+
dispatch({ type: 'OVERLAY_ACTIVATE' });
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
subscriptions.push(
|
|
347
|
+
NativeShortKitModule.onOverlayReset((_event) => {
|
|
348
|
+
dispatch({ type: 'OVERLAY_RESET' });
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
subscriptions.push(
|
|
353
|
+
NativeShortKitModule.onOverlayFadeOut((_event) => {
|
|
354
|
+
dispatch({ type: 'OVERLAY_FADE_OUT' });
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
subscriptions.push(
|
|
359
|
+
NativeShortKitModule.onOverlayRestore((_event) => {
|
|
360
|
+
dispatch({ type: 'OVERLAY_RESTORE' });
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Overlay tap events
|
|
365
|
+
subscriptions.push(
|
|
366
|
+
NativeShortKitModule.onOverlayTap((_event) => {
|
|
367
|
+
dispatch({ type: 'OVERLAY_TAP' });
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
subscriptions.push(
|
|
372
|
+
NativeShortKitModule.onOverlayDoubleTap((event) => {
|
|
373
|
+
dispatch({
|
|
374
|
+
type: 'OVERLAY_DOUBLE_TAP',
|
|
375
|
+
payload: { x: event.x, y: event.y },
|
|
376
|
+
});
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Note: Feed-level callback events (onDidLoop, onFeedTransition,
|
|
381
|
+
// onShareTapped, etc.) are subscribed by the ShortKitFeed component
|
|
382
|
+
// (Task 11), not here. The provider only manages state-driving events.
|
|
383
|
+
|
|
384
|
+
return () => {
|
|
385
|
+
for (const sub of subscriptions) {
|
|
386
|
+
sub.remove();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}, []);
|
|
390
|
+
|
|
391
|
+
// -----------------------------------------------------------------------
|
|
392
|
+
// Commands (stable refs)
|
|
393
|
+
// -----------------------------------------------------------------------
|
|
394
|
+
const play = useCallback(() => {
|
|
395
|
+
NativeShortKitModule?.play();
|
|
396
|
+
}, []);
|
|
397
|
+
|
|
398
|
+
const pause = useCallback(() => {
|
|
399
|
+
NativeShortKitModule?.pause();
|
|
400
|
+
}, []);
|
|
401
|
+
|
|
402
|
+
const seek = useCallback((seconds: number) => {
|
|
403
|
+
NativeShortKitModule?.seek(seconds);
|
|
404
|
+
}, []);
|
|
405
|
+
|
|
406
|
+
const seekAndPlay = useCallback((seconds: number) => {
|
|
407
|
+
NativeShortKitModule?.seekAndPlay(seconds);
|
|
408
|
+
}, []);
|
|
409
|
+
|
|
410
|
+
const skipToNext = useCallback(() => {
|
|
411
|
+
NativeShortKitModule?.skipToNext();
|
|
412
|
+
}, []);
|
|
413
|
+
|
|
414
|
+
const skipToPrevious = useCallback(() => {
|
|
415
|
+
NativeShortKitModule?.skipToPrevious();
|
|
416
|
+
}, []);
|
|
417
|
+
|
|
418
|
+
const setMuted = useCallback((muted: boolean) => {
|
|
419
|
+
NativeShortKitModule?.setMuted(muted);
|
|
420
|
+
}, []);
|
|
421
|
+
|
|
422
|
+
const setPlaybackRate = useCallback((rate: number) => {
|
|
423
|
+
NativeShortKitModule?.setPlaybackRate(rate);
|
|
424
|
+
}, []);
|
|
425
|
+
|
|
426
|
+
const setCaptionsEnabled = useCallback((enabled: boolean) => {
|
|
427
|
+
NativeShortKitModule?.setCaptionsEnabled(enabled);
|
|
428
|
+
}, []);
|
|
429
|
+
|
|
430
|
+
const selectCaptionTrack = useCallback((language: string) => {
|
|
431
|
+
NativeShortKitModule?.selectCaptionTrack(language);
|
|
432
|
+
}, []);
|
|
433
|
+
|
|
434
|
+
const sendContentSignal = useCallback((signal: ContentSignal) => {
|
|
435
|
+
NativeShortKitModule?.sendContentSignal(signal);
|
|
436
|
+
}, []);
|
|
437
|
+
|
|
438
|
+
const setMaxBitrate = useCallback((bitrate: number) => {
|
|
439
|
+
NativeShortKitModule?.setMaxBitrate(bitrate);
|
|
440
|
+
}, []);
|
|
441
|
+
|
|
442
|
+
const setUserIdCmd = useCallback((id: string) => {
|
|
443
|
+
NativeShortKitModule?.setUserId(id);
|
|
444
|
+
}, []);
|
|
445
|
+
|
|
446
|
+
const clearUserIdCmd = useCallback(() => {
|
|
447
|
+
NativeShortKitModule?.clearUserId();
|
|
448
|
+
}, []);
|
|
449
|
+
|
|
450
|
+
// -----------------------------------------------------------------------
|
|
451
|
+
// Context value (memoized to avoid unnecessary re-renders)
|
|
452
|
+
// -----------------------------------------------------------------------
|
|
453
|
+
const value: ShortKitContextValue = useMemo(
|
|
454
|
+
() => ({
|
|
455
|
+
// State
|
|
456
|
+
playerState: state.playerState,
|
|
457
|
+
currentItem: state.currentItem,
|
|
458
|
+
nextItem: state.nextItem,
|
|
459
|
+
time: state.time,
|
|
460
|
+
isMuted: state.isMuted,
|
|
461
|
+
playbackRate: state.playbackRate,
|
|
462
|
+
captionsEnabled: state.captionsEnabled,
|
|
463
|
+
activeCaptionTrack: state.activeCaptionTrack,
|
|
464
|
+
activeCue: state.activeCue,
|
|
465
|
+
prefetchedAheadCount: state.prefetchedAheadCount,
|
|
466
|
+
isActive: state.isActive,
|
|
467
|
+
isTransitioning: state.isTransitioning,
|
|
468
|
+
lastOverlayTap: state.lastOverlayTap,
|
|
469
|
+
lastOverlayDoubleTap: state.lastOverlayDoubleTap,
|
|
470
|
+
|
|
471
|
+
// Commands
|
|
472
|
+
play,
|
|
473
|
+
pause,
|
|
474
|
+
seek,
|
|
475
|
+
seekAndPlay,
|
|
476
|
+
skipToNext,
|
|
477
|
+
skipToPrevious,
|
|
478
|
+
setMuted,
|
|
479
|
+
setPlaybackRate,
|
|
480
|
+
setCaptionsEnabled,
|
|
481
|
+
selectCaptionTrack,
|
|
482
|
+
sendContentSignal,
|
|
483
|
+
setMaxBitrate,
|
|
484
|
+
setUserId: setUserIdCmd,
|
|
485
|
+
clearUserId: clearUserIdCmd,
|
|
486
|
+
|
|
487
|
+
// Internal — consumed by ShortKitFeed to pass to OverlayManager
|
|
488
|
+
_overlayConfig: config.overlay ?? 'none',
|
|
489
|
+
}),
|
|
490
|
+
[
|
|
491
|
+
state.playerState,
|
|
492
|
+
state.currentItem,
|
|
493
|
+
state.nextItem,
|
|
494
|
+
state.time,
|
|
495
|
+
state.isMuted,
|
|
496
|
+
state.playbackRate,
|
|
497
|
+
state.captionsEnabled,
|
|
498
|
+
state.activeCaptionTrack,
|
|
499
|
+
state.activeCue,
|
|
500
|
+
state.prefetchedAheadCount,
|
|
501
|
+
state.isActive,
|
|
502
|
+
state.isTransitioning,
|
|
503
|
+
state.lastOverlayTap,
|
|
504
|
+
state.lastOverlayDoubleTap,
|
|
505
|
+
play,
|
|
506
|
+
pause,
|
|
507
|
+
seek,
|
|
508
|
+
seekAndPlay,
|
|
509
|
+
skipToNext,
|
|
510
|
+
skipToPrevious,
|
|
511
|
+
setMuted,
|
|
512
|
+
setPlaybackRate,
|
|
513
|
+
setCaptionsEnabled,
|
|
514
|
+
selectCaptionTrack,
|
|
515
|
+
sendContentSignal,
|
|
516
|
+
setMaxBitrate,
|
|
517
|
+
setUserIdCmd,
|
|
518
|
+
clearUserIdCmd,
|
|
519
|
+
config.overlay,
|
|
520
|
+
],
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
return (
|
|
524
|
+
<ShortKitContext.Provider value={value}>{children}</ShortKitContext.Provider>
|
|
525
|
+
);
|
|
526
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export { ShortKitProvider } from './ShortKitProvider';
|
|
2
|
+
export { ShortKitFeed } from './ShortKitFeed';
|
|
3
|
+
export { useShortKitPlayer } from './useShortKitPlayer';
|
|
4
|
+
export { useShortKit } from './useShortKit';
|
|
5
|
+
export type {
|
|
6
|
+
FeedConfig,
|
|
7
|
+
FeedHeight,
|
|
8
|
+
OverlayConfig,
|
|
9
|
+
CarouselMode,
|
|
10
|
+
SurveyMode,
|
|
11
|
+
|
|
12
|
+
ContentItem,
|
|
13
|
+
JSONValue,
|
|
14
|
+
CaptionTrack,
|
|
15
|
+
PlayerTime,
|
|
16
|
+
PlayerState,
|
|
17
|
+
LoopEvent,
|
|
18
|
+
FeedTransitionEvent,
|
|
19
|
+
FormatChangeEvent,
|
|
20
|
+
ContentSignal,
|
|
21
|
+
SurveyOption,
|
|
22
|
+
ShortKitError,
|
|
23
|
+
ShortKitProviderProps,
|
|
24
|
+
ShortKitFeedProps,
|
|
25
|
+
ShortKitPlayerState,
|
|
26
|
+
} from './types';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FeedConfig,
|
|
3
|
+
ContentItem,
|
|
4
|
+
PlayerState,
|
|
5
|
+
PlayerTime,
|
|
6
|
+
} from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Serialized FeedConfig with JSON strings for complex fields.
|
|
10
|
+
* Used by ShortKitFeedView native component props.
|
|
11
|
+
*/
|
|
12
|
+
export interface SerializedFeedConfig {
|
|
13
|
+
feedHeight: string;
|
|
14
|
+
overlay: string;
|
|
15
|
+
carouselMode: string;
|
|
16
|
+
surveyMode: string;
|
|
17
|
+
muteOnStart: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Serialize FeedConfig for the bridge.
|
|
22
|
+
* Complex union types are JSON-stringified; the `component` field on custom
|
|
23
|
+
* overlays is stripped because React component references cannot cross the bridge.
|
|
24
|
+
*/
|
|
25
|
+
export function serializeFeedConfig(config: FeedConfig): SerializedFeedConfig {
|
|
26
|
+
let overlay: string;
|
|
27
|
+
const rawOverlay = config.overlay ?? 'none';
|
|
28
|
+
if (typeof rawOverlay === 'string') {
|
|
29
|
+
overlay = JSON.stringify(rawOverlay);
|
|
30
|
+
} else {
|
|
31
|
+
// Strip the component ref — it stays on the JS side for OverlayManager
|
|
32
|
+
overlay = JSON.stringify({ type: 'custom' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
feedHeight: JSON.stringify(config.feedHeight ?? { type: 'fullscreen' }),
|
|
37
|
+
overlay,
|
|
38
|
+
carouselMode: JSON.stringify(config.carouselMode ?? 'none'),
|
|
39
|
+
surveyMode: JSON.stringify(config.surveyMode ?? 'none'),
|
|
40
|
+
muteOnStart: config.muteOnStart ?? true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Serialize a FeedConfig into a single JSON string for the TurboModule
|
|
46
|
+
* `initialize(config:)` parameter.
|
|
47
|
+
*/
|
|
48
|
+
export function serializeFeedConfigForModule(config: FeedConfig): string {
|
|
49
|
+
const serialized = serializeFeedConfig(config);
|
|
50
|
+
return JSON.stringify(serialized);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Deserialize PlayerState from a bridge event.
|
|
55
|
+
* The native side sends `{ state: string; errorMessage?: string }`.
|
|
56
|
+
*/
|
|
57
|
+
export function deserializePlayerState(event: {
|
|
58
|
+
state: string;
|
|
59
|
+
errorMessage?: string;
|
|
60
|
+
}): PlayerState {
|
|
61
|
+
if (event.state === 'error' && event.errorMessage) {
|
|
62
|
+
return { error: event.errorMessage };
|
|
63
|
+
}
|
|
64
|
+
return event.state as Exclude<PlayerState, { error: string }>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Deserialize a ContentItem from a bridge JSON string.
|
|
69
|
+
* Returns null for null/undefined input or malformed JSON.
|
|
70
|
+
*/
|
|
71
|
+
export function deserializeContentItem(
|
|
72
|
+
json: string | null | undefined,
|
|
73
|
+
): ContentItem | null {
|
|
74
|
+
if (!json) return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(json) as ContentItem;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Deserialize PlayerTime from a bridge event.
|
|
84
|
+
*/
|
|
85
|
+
export function deserializePlayerTime(event: {
|
|
86
|
+
current: number;
|
|
87
|
+
duration: number;
|
|
88
|
+
buffered: number;
|
|
89
|
+
}): PlayerTime {
|
|
90
|
+
return {
|
|
91
|
+
current: event.current,
|
|
92
|
+
duration: event.duration,
|
|
93
|
+
buffered: event.buffered,
|
|
94
|
+
};
|
|
95
|
+
}
|