@mux/mux-react-native-player 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/MuxReactNativePlayer.podspec +37 -0
- package/README.md +134 -0
- package/android/build.gradle +33 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/mux/reactnativeplayer/MuxReactNativePlayerModule.kt +135 -0
- package/android/src/main/java/com/mux/reactnativeplayer/MuxVideoRecords.kt +174 -0
- package/android/src/main/java/com/mux/reactnativeplayer/MuxVideoView.kt +452 -0
- package/android/src/main/res/layout/mux_video_player_view.xml +6 -0
- package/assets/MuxRobot_02.gif +0 -0
- package/assets/MuxRobot_02@2x.gif +0 -0
- package/assets/MuxRobot_03.gif +0 -0
- package/assets/MuxRobot_03@2x.gif +0 -0
- package/assets/MuxRobot_04.gif +0 -0
- package/assets/MuxRobot_04@2x.gif +0 -0
- package/assets/MuxRobot_05.gif +0 -0
- package/assets/MuxRobot_05@2x.gif +0 -0
- package/build/MuxVideoControls.d.ts +21 -0
- package/build/MuxVideoControls.d.ts.map +1 -0
- package/build/MuxVideoControls.js +1032 -0
- package/build/MuxVideoPlayer.d.ts +59 -0
- package/build/MuxVideoPlayer.d.ts.map +1 -0
- package/build/MuxVideoPlayer.js +265 -0
- package/build/MuxVideoView.d.ts +39 -0
- package/build/MuxVideoView.d.ts.map +1 -0
- package/build/MuxVideoView.js +254 -0
- package/build/NativeMuxVideoView.d.ts +5 -0
- package/build/NativeMuxVideoView.d.ts.map +1 -0
- package/build/NativeMuxVideoView.js +4 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/normalizeSource.d.ts +7 -0
- package/build/normalizeSource.d.ts.map +1 -0
- package/build/normalizeSource.js +76 -0
- package/build/screenOrientation.d.ts +3 -0
- package/build/screenOrientation.d.ts.map +1 -0
- package/build/screenOrientation.js +38 -0
- package/build/types.d.ts +170 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +1 -0
- package/expo-module.config.json +13 -0
- package/ios/MuxReactNativePlayerModule.swift +139 -0
- package/ios/MuxVideoRecords.swift +212 -0
- package/ios/MuxVideoView.swift +502 -0
- package/package.json +69 -0
- package/plugin/index.d.ts +11 -0
- package/plugin/index.js +1 -0
- package/plugin/withMuxReactNativePlayer.js +203 -0
- package/src/MuxVideoControls.tsx +1772 -0
- package/src/MuxVideoPlayer.ts +338 -0
- package/src/MuxVideoView.tsx +412 -0
- package/src/NativeMuxVideoView.ts +15 -0
- package/src/index.ts +32 -0
- package/src/normalizeSource.ts +101 -0
- package/src/screenOrientation.ts +46 -0
- package/src/types.ts +228 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Dimensions, Modal, Platform, StyleSheet, View } from 'react-native';
|
|
3
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
4
|
+
|
|
5
|
+
const IOS_LANDSCAPE_SAFE_INSET = 50;
|
|
6
|
+
|
|
7
|
+
import { MuxVideoPlayer } from './MuxVideoPlayer';
|
|
8
|
+
import { MuxVideoControls } from './MuxVideoControls';
|
|
9
|
+
import NativeMuxVideoView from './NativeMuxVideoView';
|
|
10
|
+
import {
|
|
11
|
+
lockOrientationLandscape,
|
|
12
|
+
unlockOrientation,
|
|
13
|
+
} from './screenOrientation';
|
|
14
|
+
import type {
|
|
15
|
+
MuxNativeViewRef,
|
|
16
|
+
MuxVideoChapter,
|
|
17
|
+
MuxVideoKeyMoment,
|
|
18
|
+
MuxVideoSource,
|
|
19
|
+
MuxVideoSummary,
|
|
20
|
+
MuxVideoViewProps,
|
|
21
|
+
} from './types';
|
|
22
|
+
|
|
23
|
+
export type MuxVideoViewRef = {
|
|
24
|
+
play: () => Promise<void>;
|
|
25
|
+
pause: () => Promise<void>;
|
|
26
|
+
replay: () => Promise<void>;
|
|
27
|
+
seekBy: (seconds: number) => Promise<void>;
|
|
28
|
+
seekTo: (seconds: number) => Promise<void>;
|
|
29
|
+
setMuted: (muted: boolean) => Promise<void>;
|
|
30
|
+
setVolume: (volume: number) => Promise<void>;
|
|
31
|
+
setLoop: (loop: boolean) => Promise<void>;
|
|
32
|
+
setPlaybackRate: (rate: number) => Promise<void>;
|
|
33
|
+
setCaptionTrack: (trackId: string | null) => Promise<void>;
|
|
34
|
+
release: () => Promise<void>;
|
|
35
|
+
enterFullscreen: () => void;
|
|
36
|
+
exitFullscreen: () => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const MuxVideoView = React.forwardRef<MuxVideoViewRef, MuxVideoViewProps>(
|
|
40
|
+
(
|
|
41
|
+
{
|
|
42
|
+
player,
|
|
43
|
+
controls,
|
|
44
|
+
controlsTheme,
|
|
45
|
+
robots,
|
|
46
|
+
nativeControls = true,
|
|
47
|
+
contentFit = 'contain',
|
|
48
|
+
allowsFullscreen = true,
|
|
49
|
+
allowsPictureInPicture = false,
|
|
50
|
+
timeUpdateEventInterval = 0.5,
|
|
51
|
+
startupBufferDuration,
|
|
52
|
+
onStatusChange,
|
|
53
|
+
onPlayingChange,
|
|
54
|
+
onTimeUpdate,
|
|
55
|
+
onSourceLoad,
|
|
56
|
+
onSourceError,
|
|
57
|
+
onFullscreenChange,
|
|
58
|
+
...viewProps
|
|
59
|
+
},
|
|
60
|
+
ref
|
|
61
|
+
) => {
|
|
62
|
+
const nativeRef = React.useRef<MuxNativeViewRef>(null);
|
|
63
|
+
const [fullscreen, setFullscreen] = React.useState(false);
|
|
64
|
+
const [fullscreenReady, setFullscreenReady] = React.useState(false);
|
|
65
|
+
const enteredViaRotationRef = React.useRef(false);
|
|
66
|
+
const suppressRotationEntryRef = React.useRef(false);
|
|
67
|
+
const pendingExitRef = React.useRef(false);
|
|
68
|
+
const [generatedSummary, setGeneratedSummary] = React.useState<MuxVideoSummary>();
|
|
69
|
+
const [generatedChapters, setGeneratedChapters] = React.useState<MuxVideoChapter[]>();
|
|
70
|
+
const [generatedKeyMoments, setGeneratedKeyMoments] = React.useState<MuxVideoKeyMoment[]>();
|
|
71
|
+
const snapshot = React.useSyncExternalStore(
|
|
72
|
+
player._subscribe,
|
|
73
|
+
player._getSnapshot,
|
|
74
|
+
player._getSnapshot
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const enterFullscreen = React.useCallback(() => {
|
|
78
|
+
player._markResumePoint();
|
|
79
|
+
enteredViaRotationRef.current = false;
|
|
80
|
+
suppressRotationEntryRef.current = false;
|
|
81
|
+
pendingExitRef.current = false;
|
|
82
|
+
const { width, height } = Dimensions.get('window');
|
|
83
|
+
setFullscreenReady(width > height);
|
|
84
|
+
setFullscreen(true);
|
|
85
|
+
}, [player]);
|
|
86
|
+
const beginExit = React.useCallback(() => {
|
|
87
|
+
player._markResumePoint();
|
|
88
|
+
enteredViaRotationRef.current = false;
|
|
89
|
+
suppressRotationEntryRef.current = true;
|
|
90
|
+
setFullscreenReady(false);
|
|
91
|
+
const { width, height } = Dimensions.get('window');
|
|
92
|
+
if (width <= height) {
|
|
93
|
+
pendingExitRef.current = false;
|
|
94
|
+
setFullscreen(false);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
pendingExitRef.current = true;
|
|
98
|
+
void unlockOrientation();
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
if (pendingExitRef.current) {
|
|
101
|
+
pendingExitRef.current = false;
|
|
102
|
+
setFullscreen(false);
|
|
103
|
+
}
|
|
104
|
+
}, 600);
|
|
105
|
+
}, [player]);
|
|
106
|
+
const exitFullscreen = React.useCallback(() => {
|
|
107
|
+
beginExit();
|
|
108
|
+
}, [beginExit]);
|
|
109
|
+
const toggleFullscreen = React.useCallback(() => {
|
|
110
|
+
if (fullscreen) {
|
|
111
|
+
beginExit();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
player._markResumePoint();
|
|
115
|
+
enteredViaRotationRef.current = false;
|
|
116
|
+
suppressRotationEntryRef.current = false;
|
|
117
|
+
pendingExitRef.current = false;
|
|
118
|
+
const { width, height } = Dimensions.get('window');
|
|
119
|
+
setFullscreenReady(width > height);
|
|
120
|
+
setFullscreen(true);
|
|
121
|
+
}, [beginExit, fullscreen, player]);
|
|
122
|
+
|
|
123
|
+
React.useEffect(() => {
|
|
124
|
+
if (!allowsFullscreen) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const subscription = Dimensions.addEventListener('change', ({ window }) => {
|
|
128
|
+
const isLandscape = window.width > window.height;
|
|
129
|
+
if (!isLandscape) {
|
|
130
|
+
suppressRotationEntryRef.current = false;
|
|
131
|
+
if (pendingExitRef.current) {
|
|
132
|
+
pendingExitRef.current = false;
|
|
133
|
+
setFullscreenReady(false);
|
|
134
|
+
setFullscreen(false);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
} else if (pendingExitRef.current) {
|
|
138
|
+
pendingExitRef.current = false;
|
|
139
|
+
}
|
|
140
|
+
setFullscreenReady(isLandscape);
|
|
141
|
+
setFullscreen(prev => {
|
|
142
|
+
if (isLandscape && !prev) {
|
|
143
|
+
if (suppressRotationEntryRef.current) {
|
|
144
|
+
return prev;
|
|
145
|
+
}
|
|
146
|
+
enteredViaRotationRef.current = true;
|
|
147
|
+
player._markResumePoint();
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (!isLandscape && prev && enteredViaRotationRef.current) {
|
|
151
|
+
enteredViaRotationRef.current = false;
|
|
152
|
+
player._markResumePoint();
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return prev;
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
return () => subscription.remove();
|
|
159
|
+
}, [allowsFullscreen, player]);
|
|
160
|
+
|
|
161
|
+
React.useImperativeHandle(
|
|
162
|
+
ref,
|
|
163
|
+
() => ({
|
|
164
|
+
play: () => player.play(),
|
|
165
|
+
pause: () => player.pause(),
|
|
166
|
+
replay: () => player.replay(),
|
|
167
|
+
seekBy: seconds => player.seekBy(seconds),
|
|
168
|
+
seekTo: seconds => player.seekTo(seconds),
|
|
169
|
+
setMuted: muted => player.setMuted(muted),
|
|
170
|
+
setVolume: volume => player.setVolume(volume),
|
|
171
|
+
setLoop: loop => player.setLoop(loop),
|
|
172
|
+
setPlaybackRate: rate => player.setPlaybackRate(rate),
|
|
173
|
+
setCaptionTrack: trackId => player.setCaptionTrack(trackId),
|
|
174
|
+
release: () => player.release(),
|
|
175
|
+
enterFullscreen,
|
|
176
|
+
exitFullscreen,
|
|
177
|
+
}),
|
|
178
|
+
[player, enterFullscreen, exitFullscreen]
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
React.useEffect(() => {
|
|
182
|
+
player._attachNativeRef(nativeRef.current);
|
|
183
|
+
return () => {
|
|
184
|
+
player._attachNativeRef(null);
|
|
185
|
+
};
|
|
186
|
+
}, [player, fullscreen]);
|
|
187
|
+
|
|
188
|
+
React.useEffect(() => {
|
|
189
|
+
onFullscreenChange?.(fullscreen);
|
|
190
|
+
if (!fullscreen) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const entryViaRotation = enteredViaRotationRef.current;
|
|
194
|
+
if (!entryViaRotation) {
|
|
195
|
+
void lockOrientationLandscape();
|
|
196
|
+
}
|
|
197
|
+
return () => {
|
|
198
|
+
if (!entryViaRotation) {
|
|
199
|
+
void unlockOrientation();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}, [fullscreen, onFullscreenChange]);
|
|
203
|
+
|
|
204
|
+
const controlsMode = controls ?? (nativeControls ? 'native' : 'none');
|
|
205
|
+
const showCustomControls = controlsMode === 'custom';
|
|
206
|
+
const showNativeControls = controlsMode === 'native';
|
|
207
|
+
const controlsRobots = React.useMemo(
|
|
208
|
+
() =>
|
|
209
|
+
robots && robots.assetId == null && snapshot.source?.assetId
|
|
210
|
+
? { ...robots, assetId: snapshot.source.assetId }
|
|
211
|
+
: robots,
|
|
212
|
+
[robots, snapshot.source?.assetId]
|
|
213
|
+
);
|
|
214
|
+
const robotsAssetId = controlsRobots?.assetId;
|
|
215
|
+
|
|
216
|
+
React.useEffect(() => {
|
|
217
|
+
setGeneratedSummary(undefined);
|
|
218
|
+
setGeneratedChapters(undefined);
|
|
219
|
+
setGeneratedKeyMoments(undefined);
|
|
220
|
+
}, [robotsAssetId]);
|
|
221
|
+
|
|
222
|
+
const sharedNativeProps = {
|
|
223
|
+
startupBufferDuration,
|
|
224
|
+
source: snapshot.source,
|
|
225
|
+
playWhenReady: snapshot.shouldPlay,
|
|
226
|
+
muted: snapshot.muted,
|
|
227
|
+
volume: snapshot.volume,
|
|
228
|
+
loop: snapshot.loop,
|
|
229
|
+
playbackRate: snapshot.playbackRate,
|
|
230
|
+
contentFit,
|
|
231
|
+
allowsFullscreen,
|
|
232
|
+
allowsPictureInPicture,
|
|
233
|
+
timeUpdateEventInterval,
|
|
234
|
+
onStatusChange: (event: { nativeEvent: any }) => {
|
|
235
|
+
player._handleStatusChange(event.nativeEvent);
|
|
236
|
+
onStatusChange?.(event.nativeEvent);
|
|
237
|
+
},
|
|
238
|
+
onPlayingChange: (event: { nativeEvent: any }) => {
|
|
239
|
+
onPlayingChange?.(event.nativeEvent);
|
|
240
|
+
},
|
|
241
|
+
onTimeUpdate: (event: { nativeEvent: any }) => {
|
|
242
|
+
player._handleTimeUpdate(event.nativeEvent);
|
|
243
|
+
onTimeUpdate?.(event.nativeEvent);
|
|
244
|
+
},
|
|
245
|
+
onSourceLoad: (event: { nativeEvent: any }) => {
|
|
246
|
+
player._handleSourceLoad(event.nativeEvent);
|
|
247
|
+
onSourceLoad?.(event.nativeEvent);
|
|
248
|
+
},
|
|
249
|
+
onSourceError: (event: { nativeEvent: any }) => {
|
|
250
|
+
player._handleSourceError(event.nativeEvent);
|
|
251
|
+
onSourceError?.(event.nativeEvent);
|
|
252
|
+
},
|
|
253
|
+
} as const;
|
|
254
|
+
|
|
255
|
+
if (showNativeControls) {
|
|
256
|
+
return (
|
|
257
|
+
<NativeMuxVideoView
|
|
258
|
+
{...viewProps}
|
|
259
|
+
{...sharedNativeProps}
|
|
260
|
+
ref={nativeRef}
|
|
261
|
+
nativeControls
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const renderManagedBody = (
|
|
267
|
+
containerStyle: StyleProp<ViewStyle>,
|
|
268
|
+
controlsInset: { left: number; right: number } = { left: 0, right: 0 }
|
|
269
|
+
) => (
|
|
270
|
+
<View style={containerStyle}>
|
|
271
|
+
<NativeMuxVideoView
|
|
272
|
+
{...sharedNativeProps}
|
|
273
|
+
ref={nativeRef}
|
|
274
|
+
nativeControls={false}
|
|
275
|
+
style={StyleSheet.absoluteFill}
|
|
276
|
+
/>
|
|
277
|
+
{showCustomControls ? (
|
|
278
|
+
<View
|
|
279
|
+
pointerEvents="box-none"
|
|
280
|
+
style={{
|
|
281
|
+
position: 'absolute',
|
|
282
|
+
top: 0,
|
|
283
|
+
bottom: 0,
|
|
284
|
+
left: controlsInset.left,
|
|
285
|
+
right: controlsInset.right,
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
<MuxVideoControls
|
|
289
|
+
player={player}
|
|
290
|
+
status={snapshot.status}
|
|
291
|
+
shouldPlay={snapshot.shouldPlay}
|
|
292
|
+
theme={controlsTheme}
|
|
293
|
+
robots={controlsRobots}
|
|
294
|
+
allowsFullscreen={allowsFullscreen}
|
|
295
|
+
isFullscreen={fullscreen}
|
|
296
|
+
onToggleFullscreen={allowsFullscreen ? toggleFullscreen : undefined}
|
|
297
|
+
generatedSummary={generatedSummary}
|
|
298
|
+
generatedChapters={generatedChapters}
|
|
299
|
+
generatedKeyMoments={generatedKeyMoments}
|
|
300
|
+
onGeneratedSummaryChange={setGeneratedSummary}
|
|
301
|
+
onGeneratedChaptersChange={setGeneratedChapters}
|
|
302
|
+
onGeneratedKeyMomentsChange={setGeneratedKeyMoments}
|
|
303
|
+
/>
|
|
304
|
+
</View>
|
|
305
|
+
) : null}
|
|
306
|
+
</View>
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const inlineContainerStyle: StyleProp<ViewStyle> = [
|
|
310
|
+
styles.customControlsContainer,
|
|
311
|
+
viewProps.style,
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
if (!fullscreen) {
|
|
315
|
+
return renderManagedBody(inlineContainerStyle);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const fullscreenControlsInset =
|
|
319
|
+
Platform.OS === 'ios' && fullscreenReady
|
|
320
|
+
? { left: IOS_LANDSCAPE_SAFE_INSET, right: IOS_LANDSCAPE_SAFE_INSET }
|
|
321
|
+
: { left: 0, right: 0 };
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<>
|
|
325
|
+
<View
|
|
326
|
+
{...viewProps}
|
|
327
|
+
style={[inlineContainerStyle, styles.placeholderInline]}
|
|
328
|
+
pointerEvents="none"
|
|
329
|
+
/>
|
|
330
|
+
<Modal
|
|
331
|
+
animationType="fade"
|
|
332
|
+
visible
|
|
333
|
+
supportedOrientations={[
|
|
334
|
+
'portrait',
|
|
335
|
+
'landscape',
|
|
336
|
+
'landscape-left',
|
|
337
|
+
'landscape-right',
|
|
338
|
+
]}
|
|
339
|
+
statusBarTranslucent
|
|
340
|
+
presentationStyle="overFullScreen"
|
|
341
|
+
transparent
|
|
342
|
+
onRequestClose={exitFullscreen}
|
|
343
|
+
>
|
|
344
|
+
<View style={styles.fullscreenContainer}>
|
|
345
|
+
{renderManagedBody(StyleSheet.absoluteFill, fullscreenControlsInset)}
|
|
346
|
+
{!fullscreenReady ? (
|
|
347
|
+
<View
|
|
348
|
+
pointerEvents="none"
|
|
349
|
+
style={[StyleSheet.absoluteFill, styles.fullscreenCover]}
|
|
350
|
+
/>
|
|
351
|
+
) : null}
|
|
352
|
+
</View>
|
|
353
|
+
</Modal>
|
|
354
|
+
</>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
MuxVideoView.displayName = 'MuxVideoView';
|
|
360
|
+
|
|
361
|
+
export function useMuxVideoPlayer(
|
|
362
|
+
source?: MuxVideoSource,
|
|
363
|
+
setup?: (player: MuxVideoPlayer) => void
|
|
364
|
+
): MuxVideoPlayer {
|
|
365
|
+
const playerRef = React.useRef<MuxVideoPlayer | null>(null);
|
|
366
|
+
const sourceKey = source == null ? undefined : JSON.stringify(source);
|
|
367
|
+
|
|
368
|
+
if (playerRef.current == null) {
|
|
369
|
+
playerRef.current = new MuxVideoPlayer(source);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
React.useEffect(() => {
|
|
373
|
+
if (source != null) {
|
|
374
|
+
playerRef.current?.replace(source);
|
|
375
|
+
}
|
|
376
|
+
}, [sourceKey]);
|
|
377
|
+
|
|
378
|
+
React.useEffect(() => {
|
|
379
|
+
if (playerRef.current && setup) {
|
|
380
|
+
setup(playerRef.current);
|
|
381
|
+
}
|
|
382
|
+
}, [setup]);
|
|
383
|
+
|
|
384
|
+
React.useEffect(() => {
|
|
385
|
+
const player = playerRef.current;
|
|
386
|
+
return () => {
|
|
387
|
+
player?.release().catch(() => {
|
|
388
|
+
// React cleanup may run after the native view has already detached.
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
}, []);
|
|
392
|
+
|
|
393
|
+
return playerRef.current;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const styles = StyleSheet.create({
|
|
397
|
+
customControlsContainer: {
|
|
398
|
+
backgroundColor: 'transparent',
|
|
399
|
+
overflow: 'hidden',
|
|
400
|
+
position: 'relative',
|
|
401
|
+
},
|
|
402
|
+
placeholderInline: {
|
|
403
|
+
backgroundColor: 'transparent',
|
|
404
|
+
},
|
|
405
|
+
fullscreenContainer: {
|
|
406
|
+
backgroundColor: '#000',
|
|
407
|
+
flex: 1,
|
|
408
|
+
},
|
|
409
|
+
fullscreenCover: {
|
|
410
|
+
backgroundColor: '#000',
|
|
411
|
+
},
|
|
412
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
requireNativeViewManager,
|
|
3
|
+
requireOptionalNativeModule,
|
|
4
|
+
} from 'expo-modules-core';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
|
|
7
|
+
import type { MuxNativeViewRef, NativeMuxVideoViewProps } from './types';
|
|
8
|
+
|
|
9
|
+
requireOptionalNativeModule('MuxReactNativePlayer');
|
|
10
|
+
|
|
11
|
+
const NativeMuxVideoView = requireNativeViewManager('MuxReactNativePlayer') as React.ComponentType<
|
|
12
|
+
NativeMuxVideoViewProps & React.RefAttributes<MuxNativeViewRef>
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
export default NativeMuxVideoView;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export { createMuxVideoPlayer, MuxVideoPlayer } from './MuxVideoPlayer';
|
|
2
|
+
export { MuxVideoView, useMuxVideoPlayer } from './MuxVideoView';
|
|
3
|
+
export { muxResolutionSupport, normalizeMuxVideoSource } from './normalizeSource';
|
|
4
|
+
export type {
|
|
5
|
+
MuxContentFit,
|
|
6
|
+
MuxVideoControls,
|
|
7
|
+
MuxVideoControlsTheme,
|
|
8
|
+
MuxCustomData,
|
|
9
|
+
MuxMaxResolution,
|
|
10
|
+
MuxMinResolution,
|
|
11
|
+
MuxNativeViewRef,
|
|
12
|
+
MuxPlaybackStatus,
|
|
13
|
+
MuxPlayerStatus,
|
|
14
|
+
MuxPlayingChangeEvent,
|
|
15
|
+
MuxRenditionOrder,
|
|
16
|
+
MuxSourceErrorEvent,
|
|
17
|
+
MuxSourceLoadEvent,
|
|
18
|
+
MuxStatusChangeEvent,
|
|
19
|
+
MuxTimeUpdateEvent,
|
|
20
|
+
MuxVideoChapter,
|
|
21
|
+
MuxVideoCaptionTrack,
|
|
22
|
+
MuxVideoClipping,
|
|
23
|
+
MuxVideoMetadata,
|
|
24
|
+
MuxVideoKeyMoment,
|
|
25
|
+
MuxVideoRobotsConfig,
|
|
26
|
+
MuxVideoRobotsContext,
|
|
27
|
+
MuxVideoSource,
|
|
28
|
+
MuxVideoSourceObject,
|
|
29
|
+
MuxVideoSummary,
|
|
30
|
+
MuxVideoViewProps,
|
|
31
|
+
} from './types';
|
|
32
|
+
export type { MuxVideoViewRef } from './MuxVideoView';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MuxCustomData,
|
|
3
|
+
MuxMaxResolution,
|
|
4
|
+
MuxMinResolution,
|
|
5
|
+
MuxVideoSource,
|
|
6
|
+
NormalizedMuxVideoSource,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
const MIN_RESOLUTION_ORDER: MuxMinResolution[] = ['480p', '540p', '720p', '1080p', '1440p', '2160p'];
|
|
10
|
+
const MAX_RESOLUTION_ORDER: MuxMaxResolution[] = ['720p', '1080p', '1440p', '2160p'];
|
|
11
|
+
const CUSTOM_DATA_KEYS = Array.from({ length: 10 }, (_, index) => `customData${index + 1}`);
|
|
12
|
+
|
|
13
|
+
export function normalizeMuxVideoSource(source: MuxVideoSource): NormalizedMuxVideoSource {
|
|
14
|
+
const input = typeof source === 'string' ? { playbackId: source } : source;
|
|
15
|
+
const playbackId = input.playbackId?.trim();
|
|
16
|
+
|
|
17
|
+
if (!playbackId) {
|
|
18
|
+
throw new Error('Mux video source requires a non-empty playbackId.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (input.drmToken && !input.playbackToken) {
|
|
22
|
+
throw new Error('Mux DRM playback requires both drmToken and playbackToken.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
validateResolutionWindow(input.minResolution, input.maxResolution);
|
|
26
|
+
validateClipping(input.clipping?.assetStartTime, input.clipping?.assetEndTime);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
...input,
|
|
30
|
+
playbackId,
|
|
31
|
+
renditionOrder: input.renditionOrder ?? 'default',
|
|
32
|
+
metadata: input.metadata
|
|
33
|
+
? {
|
|
34
|
+
...input.metadata,
|
|
35
|
+
customData: normalizeCustomData(input.metadata.customData),
|
|
36
|
+
}
|
|
37
|
+
: undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validateResolutionWindow(minResolution?: MuxMinResolution, maxResolution?: MuxMaxResolution) {
|
|
42
|
+
if (!minResolution || !maxResolution) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const minPixels = Number(minResolution.replace('p', ''));
|
|
47
|
+
const maxPixels = Number(maxResolution.replace('p', ''));
|
|
48
|
+
if (minPixels > maxPixels) {
|
|
49
|
+
throw new Error(`Mux video source minResolution (${minResolution}) cannot exceed maxResolution (${maxResolution}).`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateClipping(assetStartTime?: number, assetEndTime?: number) {
|
|
54
|
+
if (assetStartTime != null && (!Number.isFinite(assetStartTime) || assetStartTime < 0)) {
|
|
55
|
+
throw new Error('Mux video source clipping.assetStartTime must be a non-negative finite number.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (assetEndTime != null && (!Number.isFinite(assetEndTime) || assetEndTime < 0)) {
|
|
59
|
+
throw new Error('Mux video source clipping.assetEndTime must be a non-negative finite number.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (assetStartTime != null && assetEndTime != null && assetStartTime >= assetEndTime) {
|
|
63
|
+
throw new Error('Mux video source clipping.assetStartTime must be less than clipping.assetEndTime.');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeCustomData(customData?: MuxCustomData): MuxCustomData | undefined {
|
|
68
|
+
if (!customData) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const out: Record<string, string> = {};
|
|
73
|
+
const directKeys = Object.keys(customData).filter(key => CUSTOM_DATA_KEYS.includes(key));
|
|
74
|
+
|
|
75
|
+
for (const key of directKeys) {
|
|
76
|
+
const value = customData[key];
|
|
77
|
+
if (typeof value === 'string') {
|
|
78
|
+
out[key] = value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const remainingValues = Object.entries(customData)
|
|
83
|
+
.filter(([key, value]) => !CUSTOM_DATA_KEYS.includes(key) && typeof value === 'string')
|
|
84
|
+
.map(([, value]) => value as string);
|
|
85
|
+
|
|
86
|
+
for (const key of CUSTOM_DATA_KEYS) {
|
|
87
|
+
if (remainingValues.length === 0) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
if (out[key] == null) {
|
|
91
|
+
out[key] = remainingValues.shift() as string;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const muxResolutionSupport = {
|
|
99
|
+
min: MIN_RESOLUTION_ORDER,
|
|
100
|
+
max: MAX_RESOLUTION_ORDER,
|
|
101
|
+
} as const;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
2
|
+
|
|
3
|
+
type MuxReactNativePlayerModule = {
|
|
4
|
+
lockFullscreenLandscape?: () => Promise<void>;
|
|
5
|
+
unlockFullscreenOrientation?: () => Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
let cached: MuxReactNativePlayerModule | null | undefined;
|
|
9
|
+
|
|
10
|
+
function loadModule(): MuxReactNativePlayerModule | null {
|
|
11
|
+
if (cached !== undefined) {
|
|
12
|
+
return cached;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
cached = requireOptionalNativeModule(
|
|
16
|
+
'MuxReactNativePlayer'
|
|
17
|
+
) as MuxReactNativePlayerModule | null;
|
|
18
|
+
} catch {
|
|
19
|
+
cached = null;
|
|
20
|
+
}
|
|
21
|
+
return cached;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function lockOrientationLandscape(): Promise<void> {
|
|
25
|
+
const mod = loadModule();
|
|
26
|
+
if (!mod) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
await mod.lockFullscreenLandscape?.();
|
|
31
|
+
} catch {
|
|
32
|
+
// Locking can fail on devices that don't support orientation changes.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function unlockOrientation(): Promise<void> {
|
|
37
|
+
const mod = loadModule();
|
|
38
|
+
if (!mod) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
await mod.unlockFullscreenOrientation?.();
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
}
|