@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.
Files changed (56) hide show
  1. package/MuxReactNativePlayer.podspec +37 -0
  2. package/README.md +134 -0
  3. package/android/build.gradle +33 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/com/mux/reactnativeplayer/MuxReactNativePlayerModule.kt +135 -0
  6. package/android/src/main/java/com/mux/reactnativeplayer/MuxVideoRecords.kt +174 -0
  7. package/android/src/main/java/com/mux/reactnativeplayer/MuxVideoView.kt +452 -0
  8. package/android/src/main/res/layout/mux_video_player_view.xml +6 -0
  9. package/assets/MuxRobot_02.gif +0 -0
  10. package/assets/MuxRobot_02@2x.gif +0 -0
  11. package/assets/MuxRobot_03.gif +0 -0
  12. package/assets/MuxRobot_03@2x.gif +0 -0
  13. package/assets/MuxRobot_04.gif +0 -0
  14. package/assets/MuxRobot_04@2x.gif +0 -0
  15. package/assets/MuxRobot_05.gif +0 -0
  16. package/assets/MuxRobot_05@2x.gif +0 -0
  17. package/build/MuxVideoControls.d.ts +21 -0
  18. package/build/MuxVideoControls.d.ts.map +1 -0
  19. package/build/MuxVideoControls.js +1032 -0
  20. package/build/MuxVideoPlayer.d.ts +59 -0
  21. package/build/MuxVideoPlayer.d.ts.map +1 -0
  22. package/build/MuxVideoPlayer.js +265 -0
  23. package/build/MuxVideoView.d.ts +39 -0
  24. package/build/MuxVideoView.d.ts.map +1 -0
  25. package/build/MuxVideoView.js +254 -0
  26. package/build/NativeMuxVideoView.d.ts +5 -0
  27. package/build/NativeMuxVideoView.d.ts.map +1 -0
  28. package/build/NativeMuxVideoView.js +4 -0
  29. package/build/index.d.ts +6 -0
  30. package/build/index.d.ts.map +1 -0
  31. package/build/index.js +3 -0
  32. package/build/normalizeSource.d.ts +7 -0
  33. package/build/normalizeSource.d.ts.map +1 -0
  34. package/build/normalizeSource.js +76 -0
  35. package/build/screenOrientation.d.ts +3 -0
  36. package/build/screenOrientation.d.ts.map +1 -0
  37. package/build/screenOrientation.js +38 -0
  38. package/build/types.d.ts +170 -0
  39. package/build/types.d.ts.map +1 -0
  40. package/build/types.js +1 -0
  41. package/expo-module.config.json +13 -0
  42. package/ios/MuxReactNativePlayerModule.swift +139 -0
  43. package/ios/MuxVideoRecords.swift +212 -0
  44. package/ios/MuxVideoView.swift +502 -0
  45. package/package.json +69 -0
  46. package/plugin/index.d.ts +11 -0
  47. package/plugin/index.js +1 -0
  48. package/plugin/withMuxReactNativePlayer.js +203 -0
  49. package/src/MuxVideoControls.tsx +1772 -0
  50. package/src/MuxVideoPlayer.ts +338 -0
  51. package/src/MuxVideoView.tsx +412 -0
  52. package/src/NativeMuxVideoView.ts +15 -0
  53. package/src/index.ts +32 -0
  54. package/src/normalizeSource.ts +101 -0
  55. package/src/screenOrientation.ts +46 -0
  56. 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
+ }