diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 85fcb3e2..91af90bb 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -1,7 +1,10 @@
-
+
+
+
+
+
+
+
+
+
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 4cbc74bc..63f62f86 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1565,7 +1565,7 @@ PODS:
- React-logger (= 0.77.2)
- React-perflogger (= 0.77.2)
- React-utils (= 0.77.2)
- - ReactNativeVideo (7.0.0-dev.7):
+ - ReactNativeVideo (7.0.0-dev.9):
- DoubleConversion
- glog
- hermes-engine
@@ -1876,7 +1876,7 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: f334cebc0beed0a72490492e978007082c03d533
ReactCodegen: 474fbb3e4bb0f1ee6c255d1955db76e13d509269
ReactCommon: 7763e59534d58e15f8f22121cdfe319040e08888
- ReactNativeVideo: b2fade3dd3cd947936c2a0397f7f761d64bfd6d6
+ ReactNativeVideo: 04b4d110e8d2bac7cbe51be0dd4932a72d2f1404
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: 31a098f74c16780569aebd614a0f37a907de0189
diff --git a/example/src/App.tsx b/example/src/App.tsx
index ea1c294e..fd06bdb8 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -11,11 +11,16 @@ import {
View,
} from 'react-native';
import {
- createSource,
+ useEvent,
useVideoPlayer,
+ VideoPlayer,
VideoView,
+ type IgnoreSilentSwitchMode,
+ type MixAudioMode,
type onLoadData,
type onProgressData,
+ type ResizeMode,
+ type TextTrack,
type VideoPlayerStatus,
type VideoViewRef,
} from 'react-native-video';
@@ -27,50 +32,98 @@ const formatTime = (seconds: number) => {
return `${m}:${s < 10 ? '0' : ''}${s}`;
};
+// Consolidated state interface
+interface VideoSettings {
+ show: boolean;
+ volume: number;
+ muted: boolean;
+ rate: number;
+ loop: boolean;
+ showNativeControls: boolean;
+ resizeMode: ResizeMode;
+ mixAudioMode: MixAudioMode;
+ ignoreSilentSwitchMode: IgnoreSilentSwitchMode;
+ playInBackground: boolean;
+ playWhenInactive: boolean;
+}
+
+const defaultSettings: VideoSettings = {
+ show: false,
+ volume: 1,
+ muted: false,
+ rate: 1,
+ loop: false,
+ showNativeControls: false,
+ resizeMode: 'contain',
+ mixAudioMode: 'auto',
+ ignoreSilentSwitchMode: 'auto',
+ playInBackground: true,
+ playWhenInactive: false,
+};
+
const VideoDemo = () => {
const videoViewRef = React.useRef(null);
- const [show, setShow] = React.useState(false);
- const [volume, setVolume] = React.useState(1);
- const [muted, setMuted] = React.useState(false);
- const [rate, setRate] = React.useState(1);
- const [loop, setLoop] = React.useState(false);
- const [showNativeControls, setShowNativeControls] = React.useState(false);
+ const [settings, setSettings] =
+ React.useState(defaultSettings);
+ const [progress, setProgress] = React.useState(0);
+ const [events, setEvents] = React.useState<
+ Array<{ id: string; message: string; timestamp: string }>
+ >([]);
- // For demo: log last event
- const [lastEvent, setLastEvent] = React.useState('');
+ // Helper function to update settings
+ const updateSetting = (
+ key: K,
+ value: VideoSettings[K]
+ ) => {
+ setSettings((prev) => ({ ...prev, [key]: value }));
+ };
- // View event handlers
- const handleFullscreenChange = React.useCallback((fullscreen: boolean) => {
- setLastEvent(
- 'View: onFullscreenChange ' + (fullscreen ? 'entered' : 'exited')
- );
- console.log('Fullscreen:', fullscreen);
+ // Helper function to add events
+ const addEvent = React.useCallback((message: string) => {
+ const newEvent = {
+ id: `${Date.now()}-${Math.random()}`,
+ message,
+ timestamp: new Date().toLocaleTimeString(),
+ };
+ setEvents((prev) => [newEvent, ...prev.slice(0, 49)]); // Keep latest 50 events
}, []);
+ // Event handlers
+ const handleFullscreenChange = React.useCallback(
+ (fullscreen: boolean) => {
+ addEvent(
+ 'View: onFullscreenChange ' + (fullscreen ? 'entered' : 'exited')
+ );
+ },
+ [addEvent]
+ );
+
const handlePictureInPictureChange = React.useCallback(
(pipEnabled: boolean) => {
- setLastEvent(
+ addEvent(
'View: onPictureInPictureChange ' + (pipEnabled ? 'entered' : 'exited')
);
- console.log('PictureInPicture:', pipEnabled);
},
- []
+ [addEvent]
);
const handlePlayerEnd = React.useCallback(() => {
- setLastEvent('Player: onEnd');
- console.log('Video has ended');
- }, []);
+ addEvent('Player: onEnd');
+ }, [addEvent]);
- const handlePlayerLoad = React.useCallback((data: onLoadData) => {
- setLastEvent('Player: onLoad');
- console.log('Video loaded', data);
- }, []);
+ const handlePlayerLoad = React.useCallback(
+ (_data: onLoadData) => {
+ addEvent('Player: onLoad');
+ },
+ [addEvent]
+ );
- const handlePlayerBuffer = React.useCallback((buffering: boolean) => {
- setLastEvent('Player: onBuffer ' + buffering);
- console.log('Buffering:', buffering);
- }, []);
+ const handlePlayerBuffer = React.useCallback(
+ (buffering: boolean) => {
+ addEvent('Player: onBuffer ' + buffering);
+ },
+ [addEvent]
+ );
const handlePlayerProgress = React.useCallback((data: onProgressData) => {
setProgress(data.currentTime);
@@ -78,13 +131,12 @@ const VideoDemo = () => {
const handlePlayerStatusChange = React.useCallback(
(status: VideoPlayerStatus) => {
- setLastEvent('Player: onStatusChange ' + status);
- console.log('Status:', status);
+ addEvent('Player: onStatusChange ' + status);
},
- []
+ [addEvent]
);
- // Setup player and events
+ // Setup player
const player = useVideoPlayer(
{
uri: 'https://www.w3schools.com/html/movie.mp4',
@@ -96,57 +148,50 @@ const VideoDemo = () => {
],
},
(_player) => {
- _player.onEnd = handlePlayerEnd;
- _player.onLoad = handlePlayerLoad;
- _player.onBuffer = handlePlayerBuffer;
- _player.onProgress = handlePlayerProgress;
- _player.onStatusChange = handlePlayerStatusChange;
+ // Setup player
}
);
- // Sync volume, muted, rate with player
- React.useEffect(() => {
- if (!show) return;
- player.volume = volume;
- }, [volume, player, show]);
- React.useEffect(() => {
- if (!show) return;
- player.muted = muted;
- }, [muted, player, show]);
- React.useEffect(() => {
- if (!show) return;
- player.rate = rate;
- }, [rate, player, show]);
- React.useEffect(() => {
- if (!show) return;
- player.loop = loop;
- }, [loop, player, show]);
+ useEvent(player, 'onEnd', handlePlayerEnd);
+ useEvent(player, 'onLoad', handlePlayerLoad);
+ useEvent(player, 'onBuffer', handlePlayerBuffer);
+ useEvent(player, 'onProgress', handlePlayerProgress);
+ useEvent(player, 'onStatusChange', handlePlayerStatusChange);
- // Progress state
- const [progress, setProgress] = React.useState(0);
+ // Sync settings with player
+ React.useEffect(() => {
+ if (!settings.show) return;
+
+ player.volume = settings.volume;
+ player.muted = settings.muted;
+ player.rate = settings.rate;
+ player.loop = settings.loop;
+ player.playInBackground = settings.playInBackground;
+ player.playWhenInactive = settings.playWhenInactive;
+ player.mixAudioMode = settings.mixAudioMode;
+ player.ignoreSilentSwitchMode = settings.ignoreSilentSwitchMode;
+ }, [settings, player]);
- // Handlers
const handleSeek = (val: number) => {
player.seekTo(val);
setProgress(val);
};
return (
-
-
- {show ? (
+
+ {/* Video Player */}
+
+ {settings.show ? (
) : (
@@ -155,84 +200,186 @@ const VideoDemo = () => {
)}
- {/* Progress bar */}
-
- {formatTime(progress)}
-
-
- {formatTime(show ? player.duration : 0)}
-
-
-
- {/* Event log */}
-
-
- Last event: {lastEvent}
-
-
-
- {/* Controls */}
-
- player.seekTo(0)} />
- player.seekBy(-1)} />
- player.play()} label="Play" />
- player.pause()} label="Pause" />
- player.seekBy(1)} />
-
-
- {/* Volume/Mute/Speed */}
-
-
- 🔊 Volume
+ {/* Progress Controls */}
+
+
+ {formatTime(progress)}
-
-
- 🔇 Mute
-
-
-
- 🔁 Loop
-
-
-
- ⏩ Speed
-
- {rate}x
+
+ {formatTime(settings.show ? player.duration : 0)}
+
- {/* Extra actions */}
-
-
+ {/* Transport Controls */}
+
+
+ player.seekTo(0)} />
+ player.seekBy(-10)} />
+ (player.isPlaying ? player.pause() : player.play())}
+ size="large"
+ />
+ player.seekBy(10)} />
+
+
+
+ {/* Display Settings */}
+
+ Display Settings
+
+
+ updateSetting('show', value)}
+ />
+
+ updateSetting('showNativeControls', value)
+ }
+ />
+
+
+ Resize Mode
+
+ {(['contain', 'cover', 'stretch', 'none'] as const).map((mode) => (
+ updateSetting('resizeMode', mode)}
+ />
+ ))}
+
+
+
+ {/* Audio Controls */}
+
+ Audio Controls
+
+
+ Volume
+ updateSetting('volume', value)}
+ minimumTrackTintColor="#007aff"
+ maximumTrackTintColor="#e1e1e1"
+ thumbTintColor="#007aff"
+ />
+
+ {Math.round(settings.volume * 100)}%
+
+
+
+
+ Speed
+ updateSetting('rate', value)}
+ minimumTrackTintColor="#007aff"
+ maximumTrackTintColor="#e1e1e1"
+ thumbTintColor="#007aff"
+ />
+ {settings.rate}x
+
+
+
+
+ updateSetting('muted', value)}
+ />
+ updateSetting('loop', value)}
+ />
+
+
+
+ {/* Background Playback Controls */}
+
+ Background Playback
+
+ updateSetting('playInBackground', value)}
+ />
+ updateSetting('playWhenInactive', value)}
+ />
+
+
+
+ {/* Audio Settings */}
+
+ Audio Settings
+
+ Mix Audio Mode
+
+ {(['mixWithOthers', 'doNotMix', 'duckOthers', 'auto'] as const).map(
+ (mode) => (
+ updateSetting('mixAudioMode', mode)}
+ />
+ )
+ )}
+
+
+ Silent Switch Mode
+
+ {(['auto', 'ignore', 'obey'] as const).map((mode) => (
+ updateSetting('ignoreSilentSwitchMode', mode)}
+ />
+ ))}
+
+
+
+ {/* Text Track Controls */}
+
+ Text Tracks
+
+
+
+ {/* Advanced Controls */}
+
+ Advanced Controls
+
+
player.preload().catch(console.error)}
@@ -240,29 +387,25 @@ const VideoDemo = () => {
{
- const newSource = createSource(
- 'https://www.w3schools.com/html/movie.mp4'
- );
- newSource.getAssetInformationAsync().then(console.log);
+ const newSource = {
+ uri: 'https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8',
+ };
player.replaceSourceAsync(newSource);
}}
/>
-
-
setShow((prev) => !prev)}
+ label="Get Source Asset Info"
+ onPress={() => {
+ player.source
+ .getAssetInformationAsync()
+ .then((info) => {
+ console.log('Asset info:', info);
+ })
+ .catch((error) => {
+ console.error('Error getting asset info:', error);
+ });
+ }}
/>
-
- Native Controls
-
-
-
-
{
@@ -274,21 +417,7 @@ const VideoDemo = () => {
}}
/>
{
- if (videoViewRef.current) {
- videoViewRef.current.enterFullscreen();
-
- setTimeout(() => {
- videoViewRef.current?.exitFullscreen();
- }, 5000);
- }
- }}
- />
-
-
- {
if (videoViewRef.current) {
videoViewRef.current.enterPictureInPicture();
@@ -297,53 +426,100 @@ const VideoDemo = () => {
}
}}
/>
- {
- if (videoViewRef.current) {
- const canEnter =
- videoViewRef.current.canEnterPictureInPicture();
- Alert.alert(
- `${canEnter ? 'Can' : 'Cannot'} Enter Picture In Picture on this device`
- );
- } else {
- Alert.alert('No video view found');
- }
- }}
- />
- {
- if (videoViewRef.current) {
- videoViewRef.current.exitPictureInPicture();
- } else {
- Alert.alert('No video view found');
- }
- }}
- />
-
+
+
+
+ {/* Event Log */}
+
+ Event Log
+
+
+ {events.length === 0 ? (
+ No events yet
+ ) : (
+ events.map((event) => (
+
+ {event.timestamp}
+ {event.message}
+
+ ))
+ )}
+
+
);
};
-// Simple icon button
-const IconButton = ({
+// Reusable Components
+const ControlButton = ({
icon,
- label,
onPress,
+ size = 'normal',
}: {
icon: string;
- label?: string;
onPress: () => void;
+ size?: 'normal' | 'large';
}) => (
-
- {icon}
- {label && {label}}
+
+
+ {icon}
+
+
+);
+
+const SwitchControl = ({
+ label,
+ value,
+ onValueChange,
+}: {
+ label: string;
+ value: boolean;
+ onValueChange: (value: boolean) => void;
+}) => (
+
+ {label}
+
+
+);
+
+const ToggleButton = ({
+ label,
+ active,
+ onPress,
+}: {
+ label: string;
+ active: boolean;
+ onPress: () => void;
+}) => (
+
+
+ {label}
+
);
-// Simple action button
const ActionButton = ({
label,
onPress,
@@ -356,25 +532,166 @@ const ActionButton = ({
);
-const Section = ({
- title,
- children,
-}: {
- title: string;
- children: React.ReactNode;
-}) => (
- <>
- {title}
- {children}
- >
-);
+const TextTrackManager = ({ player }: { player: VideoPlayer }) => {
+ const [textTracks, setTextTracks] = React.useState([]);
+ const [selectedTrackId, setSelectedTrackId] = React.useState(
+ null
+ );
+ const [currentSelectedTrack, setCurrentSelectedTrack] =
+ React.useState(null);
+ const [trackChangeEvents, setTrackChangeEvents] = React.useState(
+ []
+ );
+
+ const loadTextTracks = React.useCallback(() => {
+ try {
+ const tracks = player.getAvailableTextTracks();
+ setTextTracks(tracks);
+
+ // Get currently selected track using the new property
+ const selectedTrack = player.selectedTrack;
+ setSelectedTrackId(selectedTrack?.id || null);
+ setCurrentSelectedTrack(selectedTrack || null);
+
+ console.log('Available text tracks:', tracks);
+ console.log('Currently selected track:', selectedTrack);
+ } catch (error) {
+ console.error('Error loading text tracks:', error);
+ }
+ }, [player]);
+
+ const selectTrack = React.useCallback(
+ (track: TextTrack) => {
+ try {
+ player.selectTextTrack(track);
+ console.log('Selected text track:', track);
+ // onTrackChange event will update the state automatically
+ } catch (error) {
+ console.error('Error selecting text track:', error);
+ }
+ },
+ [player]
+ );
+
+ const disableTextTracks = React.useCallback(() => {
+ try {
+ // Pass null to disable text tracks
+ player.selectTextTrack(null);
+ console.log('Disabled text tracks');
+ // onTrackChange event will update the state automatically
+ } catch (error) {
+ console.error('Error disabling text tracks:', error);
+ }
+ }, [player]);
+
+ useEvent(player, 'onReadyToDisplay', () => {
+ loadTextTracks();
+ });
+
+ useEvent(player, 'onTrackChange', (track) => {
+ // Update state when track changes through any means (API or native controls)
+ setCurrentSelectedTrack(track);
+ setSelectedTrackId(track?.id || null);
+
+ // Add to event log
+ const timestamp = new Date().toLocaleTimeString();
+ const eventMessage = track
+ ? `${timestamp}: Track changed to "${track.label}" (${track.id})${track.id.startsWith('external-') ? ' [External]' : ''}`
+ : `${timestamp}: All tracks disabled`;
+
+ setTrackChangeEvents((prev) => [eventMessage, ...prev.slice(0, 4)]); // Keep last 5 events
+ });
+
+ return (
+
+
+
+
+ {
+ try {
+ const selectedTrack = player.selectedTrack;
+ setCurrentSelectedTrack(selectedTrack || null);
+ setSelectedTrackId(selectedTrack?.id || null);
+ } catch (error) {
+ console.error('Error syncing selection:', error);
+ }
+ }}
+ />
+
+
+ {currentSelectedTrack && (
+
+ Currently Selected:
+
+ {currentSelectedTrack.label}
+ {currentSelectedTrack.language &&
+ ` (${currentSelectedTrack.language})`}
+ {currentSelectedTrack.id.startsWith('external-') && ' [External]'}
+
+
+ )}
+
+ {trackChangeEvents.length > 0 && (
+
+ Track Change Events:
+ {trackChangeEvents.map((event, index) => (
+
+ {event}
+
+ ))}
+
+ )}
+
+ {textTracks.length > 0 ? (
+
+
+ Available Tracks ({textTracks.length})
+
+ {textTracks.map((track) => (
+ selectTrack(track)}
+ >
+
+ {track.label} {track.language && `(${track.language})`}
+ {track.selected && ' ✓'}
+ {track.id.startsWith('external-') && ' [External]'}
+
+
+ ))}
+
+ ) : (
+
+ No text tracks available
+
+ Make sure the video is loaded. External subtitles are loaded but not
+ automatically enabled - you need to select them manually.
+
+
+ )}
+
+ );
+};
export default function App() {
const [mounted, setMounted] = React.useState(true);
+
return (
-
+
{mounted && }
-
+
setMounted((prev) => !prev)}
@@ -385,20 +702,26 @@ export default function App() {
}
const styles = StyleSheet.create({
- container: {
- width: '100%',
- paddingTop: 16,
+ app: {
+ flex: 1,
+ backgroundColor: '#f8f9fa',
},
- card: {
- width: 340,
- height: 220,
- backgroundColor: '#222',
- borderRadius: 16,
+ container: {
+ flex: 1,
+ padding: 16,
+ },
+ videoContainer: {
+ width: '100%',
+ aspectRatio: 16 / 9,
+ backgroundColor: '#000',
+ borderRadius: 12,
overflow: 'hidden',
- marginBottom: 14,
- elevation: 4,
- justifyContent: 'center',
- alignItems: 'center',
+ marginBottom: 16,
+ elevation: 3,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
},
video: {
width: '100%',
@@ -407,125 +730,283 @@ const styles = StyleSheet.create({
hiddenVideo: {
width: '100%',
height: '100%',
- backgroundColor: '#444',
+ backgroundColor: '#333',
alignItems: 'center',
justifyContent: 'center',
},
hiddenVideoText: {
color: '#fff',
- fontSize: 20,
- fontWeight: 'bold',
+ fontSize: 18,
+ fontWeight: '600',
+ },
+ section: {
+ backgroundColor: '#fff',
+ borderRadius: 12,
+ padding: 16,
+ marginBottom: 12,
+ elevation: 1,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.05,
+ shadowRadius: 2,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: '700',
+ color: '#1a1a1a',
+ marginBottom: 12,
+ },
+ subSectionTitle: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#666',
+ marginTop: 12,
+ marginBottom: 8,
},
progressRow: {
flexDirection: 'row',
alignItems: 'center',
- width: 340,
- marginBottom: 8,
},
- progress: {
+ progressSlider: {
flex: 1,
- marginHorizontal: 8,
+ marginHorizontal: 12,
},
timeText: {
- color: '#444',
- fontVariant: ['tabular-nums'],
- width: 44,
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#666',
+ width: 50,
textAlign: 'center',
+ fontVariant: ['tabular-nums'],
},
- controlsRow: {
+ transportRow: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
- marginBottom: 8,
- gap: 8,
+ gap: 12,
},
- iconButton: {
+ controlButton: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ backgroundColor: '#f0f0f0',
+ alignItems: 'center',
+ justifyContent: 'center',
+ elevation: 2,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.1,
+ shadowRadius: 2,
+ },
+ controlButtonLarge: {
+ width: 64,
+ height: 64,
+ borderRadius: 32,
+ backgroundColor: '#007aff',
+ },
+ controlIcon: {
+ fontSize: 20,
+ color: '#333',
+ },
+ controlIconLarge: {
+ fontSize: 28,
+ color: '#fff',
+ },
+ audioControls: {
+ gap: 16,
+ },
+ sliderControl: {
flexDirection: 'row',
alignItems: 'center',
- padding: 8,
- marginHorizontal: 2,
- borderRadius: 8,
- backgroundColor: '#fff',
- elevation: 2,
+ gap: 12,
},
- icon: {
- fontSize: 22,
- marginRight: 2,
- },
- iconLabel: {
+ controlLabel: {
fontSize: 14,
- color: '#222',
+ fontWeight: '600',
+ color: '#333',
+ width: 60,
},
- settingsRow: {
+ slider: {
+ flex: 1,
+ },
+ valueText: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#666',
+ width: 40,
+ textAlign: 'right',
+ },
+ switchRow: {
+ flexDirection: 'row',
+ gap: 20,
+ flexWrap: 'wrap',
+ },
+ switchColumn: {
+ flexDirection: 'column',
+ gap: 8,
+ },
+ switchControl: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ flex: 1,
+ minWidth: 120,
+ },
+ switchLabel: {
+ fontSize: 14,
+ fontWeight: '500',
+ color: '#333',
+ },
+ buttonGroup: {
flexDirection: 'row',
flexWrap: 'wrap',
- alignItems: 'flex-start',
- width: '100%',
- marginVertical: 8,
gap: 8,
- paddingHorizontal: 32,
},
- setting: {
+ toggleButton: {
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ borderRadius: 20,
+ backgroundColor: '#f0f0f0',
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ },
+ toggleButtonActive: {
+ backgroundColor: '#007aff',
+ borderColor: '#007aff',
+ },
+ toggleButtonText: {
+ fontSize: 14,
+ fontWeight: '500',
+ color: '#666',
+ textTransform: 'capitalize',
+ },
+ toggleButtonTextActive: {
+ color: '#fff',
+ fontWeight: '600',
+ },
+ actionGrid: {
flexDirection: 'row',
- alignItems: 'center',
- marginHorizontal: 4,
- backgroundColor: '#fcfcfc',
- borderRadius: 8,
- padding: 8,
- },
- settingLabel: {
- fontSize: 12,
- fontWeight: 'bold',
- color: '#555',
- marginBottom: 2,
- },
- settingSlider: {
- width: 80,
- marginVertical: 2,
- },
- settingValue: {
- fontSize: 12,
- color: '#555',
- },
- switch: {
- marginHorizontal: 8,
- borderRadius: 11,
- justifyContent: 'center',
- padding: 2,
- },
- extraRow: {
- flexDirection: 'column',
- justifyContent: 'center',
- gap: 10,
- marginVertical: 8,
- paddingHorizontal: 32,
+ flexWrap: 'wrap',
+ gap: 8,
},
actionButton: {
backgroundColor: '#007aff',
paddingHorizontal: 16,
- paddingVertical: 8,
+ paddingVertical: 10,
borderRadius: 8,
- marginHorizontal: 4,
+ elevation: 2,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.1,
+ shadowRadius: 2,
},
actionButtonText: {
color: '#fff',
- fontWeight: 'bold',
+ fontWeight: '600',
+ fontSize: 14,
},
- section: {
- padding: 8,
- marginBottom: 8,
+ eventLog: {
+ backgroundColor: '#f8f9fa',
borderRadius: 8,
- borderColor: 'black',
- borderWidth: 1,
- elevation: 2,
- width: '100%',
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 8,
+ borderLeftWidth: 3,
+ borderLeftColor: '#007aff',
+ height: 200,
},
- sectionTitle: {
- fontSize: 16,
- fontWeight: 'bold',
+ eventLogScroll: {
+ flex: 1,
+ padding: 12,
+ },
+ eventItem: {
+ paddingVertical: 4,
+ borderBottomWidth: 1,
+ borderBottomColor: '#e1e1e1',
+ },
+ eventTime: {
+ fontSize: 10,
+ color: '#999',
+ fontFamily: 'monospace',
+ marginBottom: 2,
+ },
+ eventText: {
+ fontSize: 12,
+ color: '#666',
+ fontFamily: 'monospace',
+ },
+ mountControl: {
+ padding: 16,
+ alignItems: 'center',
+ },
+ trackList: {
+ marginTop: 12,
+ },
+ trackButton: {
+ padding: 12,
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ borderRadius: 8,
+ marginBottom: 8,
+ },
+ trackButtonSelected: {
+ backgroundColor: '#007aff',
+ borderColor: '#007aff',
+ },
+ trackButtonText: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#333',
+ },
+ trackButtonTextSelected: {
+ color: '#fff',
+ fontWeight: '600',
+ },
+ noTracksText: {
+ fontSize: 14,
+ color: '#666',
+ fontWeight: '600',
+ textAlign: 'center',
+ },
+ noTracksSubText: {
+ fontSize: 12,
+ color: '#999',
+ textAlign: 'center',
+ },
+ selectedTrackInfo: {
+ backgroundColor: '#e7f4ff',
+ padding: 12,
+ borderRadius: 8,
+ borderLeftWidth: 3,
+ borderLeftColor: '#007aff',
+ marginBottom: 12,
+ },
+ selectedTrackLabel: {
+ fontSize: 12,
+ fontWeight: '600',
+ color: '#666',
marginBottom: 4,
},
+ selectedTrackText: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#007aff',
+ },
+ eventLogContainer: {
+ backgroundColor: '#f8f9fa',
+ padding: 12,
+ borderRadius: 8,
+ borderLeftWidth: 3,
+ borderLeftColor: '#28a745',
+ marginBottom: 12,
+ },
+ eventLogTitle: {
+ fontSize: 12,
+ fontWeight: '600',
+ color: '#666',
+ marginBottom: 8,
+ },
+ eventLogText: {
+ fontSize: 11,
+ color: '#333',
+ fontFamily: 'monospace',
+ marginBottom: 2,
+ },
});
diff --git a/packages/react-native-video/android/build.gradle b/packages/react-native-video/android/build.gradle
index 774653bd..f6b070de 100644
--- a/packages/react-native-video/android/build.gradle
+++ b/packages/react-native-video/android/build.gradle
@@ -231,6 +231,9 @@ dependencies {
// For loading data using the OkHttp network stack
implementation "androidx.media3:media3-datasource-okhttp:$media3_version"
+ // For Media Session
+ implementation "androidx.media3:media3-session:$media3_version"
+
if (ExoplayerDependencies.useExoplayerDash) {
implementation "androidx.media3:media3-exoplayer-dash:$media3_version"
}
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/AudioFocusManager.kt b/packages/react-native-video/android/src/main/java/com/video/core/AudioFocusManager.kt
new file mode 100644
index 00000000..1fbb3888
--- /dev/null
+++ b/packages/react-native-video/android/src/main/java/com/video/core/AudioFocusManager.kt
@@ -0,0 +1,233 @@
+package com.video.core
+
+import android.content.Context
+import android.media.AudioAttributes
+import android.media.AudioManager
+import android.media.AudioFocusRequest
+import android.os.Build
+import androidx.annotation.OptIn
+import androidx.annotation.RequiresApi
+import androidx.media3.common.util.UnstableApi
+import com.margelo.nitro.NitroModules
+import com.margelo.nitro.video.HybridVideoPlayer
+import com.margelo.nitro.video.MixAudioMode
+import kotlin.getValue
+import com.video.core.utils.Threading
+
+@OptIn(UnstableApi::class)
+class AudioFocusManager() {
+ private val players = mutableListOf()
+ private var currentMixAudioMode: MixAudioMode? = null
+ private var audioFocusRequest: AudioFocusRequest? = null
+
+ val appContext by lazy {
+ NitroModules.applicationContext ?: throw UnknownError()
+ }
+
+ private val audioManager by lazy {
+ appContext.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: throw UnknownError()
+ }
+
+ private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
+ when (focusChange) {
+ AudioManager.AUDIOFOCUS_GAIN -> {
+ unDuckActivePlayers()
+ }
+ AudioManager.AUDIOFOCUS_LOSS -> {
+ pauseActivePlayers()
+ currentMixAudioMode = null
+ audioFocusRequest = null
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
+ val mixAudioMode = determineRequiredMixMode()
+ if (mixAudioMode != MixAudioMode.MIXWITHOTHERS) {
+ pauseActivePlayers()
+ currentMixAudioMode = null
+ audioFocusRequest = null
+ }
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
+ val mixAudioMode = determineRequiredMixMode()
+ when (mixAudioMode) {
+ MixAudioMode.DONOTMIX -> pauseActivePlayers()
+ else -> duckActivePlayers()
+ }
+ }
+ }
+ }
+
+ fun registerPlayer(player: HybridVideoPlayer) {
+ if (!players.contains(player)) {
+ players.add(player)
+ }
+ }
+
+ fun unregisterPlayer(player: HybridVideoPlayer) {
+ players.remove(player)
+ if (players.isEmpty()) {
+ abandonAudioFocus()
+ } else {
+ requestAudioFocusUpdate()
+ }
+ }
+
+ fun requestAudioFocusUpdate() {
+ Threading.runOnMainThread {
+ val requiredMixMode = determineRequiredMixMode()
+
+ if (requiredMixMode == null) {
+ abandonAudioFocus()
+ return@runOnMainThread
+ }
+
+ if (currentMixAudioMode != requiredMixMode) {
+ requestAudioFocus(requiredMixMode)
+ }
+ }
+ }
+
+ private fun determineRequiredMixMode(): MixAudioMode? {
+ val activePlayers = players.filter { player ->
+ player.player?.isPlaying == true && player.player?.volume != 0f
+ }
+
+ if (activePlayers.isEmpty()) {
+ return null
+ }
+
+ val anyPlayerNeedsMixWithOthers = activePlayers.any { player ->
+ player.mixAudioMode == MixAudioMode.MIXWITHOTHERS
+ }
+
+ if (anyPlayerNeedsMixWithOthers) {
+ abandonAudioFocus()
+ return MixAudioMode.MIXWITHOTHERS
+ }
+
+ val anyPlayerNeedsExclusiveFocus = activePlayers.any { player ->
+ player.mixAudioMode == MixAudioMode.DONOTMIX
+ }
+
+ val anyPlayerNeedsDucking = activePlayers.any { player ->
+ player.mixAudioMode == MixAudioMode.DUCKOTHERS
+ }
+
+ return when {
+ anyPlayerNeedsExclusiveFocus -> MixAudioMode.DONOTMIX
+ anyPlayerNeedsDucking -> MixAudioMode.DUCKOTHERS
+ else -> MixAudioMode.AUTO
+ }
+ }
+
+ private fun requestAudioFocus(mixMode: MixAudioMode) {
+ val focusType = when (mixMode) {
+ MixAudioMode.DONOTMIX -> AudioManager.AUDIOFOCUS_GAIN
+ MixAudioMode.DUCKOTHERS -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ MixAudioMode.AUTO -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ MixAudioMode.MIXWITHOTHERS -> return // No focus needed for mix with others
+ }
+
+ val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ requestAudioFocusNew(focusType)
+ } else {
+ requestAudioFocusLegacy(focusType)
+ }
+
+ if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ currentMixAudioMode = mixMode
+ } else {
+ currentMixAudioMode = null
+ // Pause players since audio focus couldn't be obtained
+ pauseActivePlayers()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun requestAudioFocusNew(focusType: Int): Int {
+
+ audioFocusRequest = AudioFocusRequest.Builder(focusType)
+ .setAudioAttributes(
+ AudioAttributes.Builder().run {
+ setUsage(AudioAttributes.USAGE_MEDIA)
+ setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+ build()
+ }
+ )
+ .setOnAudioFocusChangeListener(audioFocusChangeListener)
+ .build()
+
+ return audioManager.requestAudioFocus(audioFocusRequest!!)
+ }
+
+ @Suppress("DEPRECATION")
+ private fun requestAudioFocusLegacy(focusType: Int): Int {
+ return audioManager.requestAudioFocus(
+ audioFocusChangeListener,
+ AudioManager.STREAM_MUSIC,
+ focusType
+ )
+ }
+
+ private fun abandonAudioFocus() {
+ if (currentMixAudioMode != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ abandonAudioFocusNew()
+ } else {
+ abandonAudioFocusLegacy()
+ }
+ currentMixAudioMode = null
+ audioFocusRequest = null
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun abandonAudioFocusNew() {
+ audioFocusRequest?.let { request ->
+ audioManager.abandonAudioFocusRequest(request)
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ private fun abandonAudioFocusLegacy() {
+ audioManager.abandonAudioFocus(audioFocusChangeListener)
+ }
+
+ private fun pauseActivePlayers() {
+ Threading.runOnMainThread {
+ players.forEach { player ->
+ player.player?.let { mediaPlayer ->
+ if (mediaPlayer.volume != 0f && mediaPlayer.isPlaying) {
+ mediaPlayer.pause()
+ }
+ }
+ }
+ }
+ }
+
+ private fun duckActivePlayers() {
+ Threading.runOnMainThread {
+ players.forEach { player ->
+ player.player?.let { mediaPlayer ->
+ // We need to duck the volume to 50%. After the audio focus is regained,
+ // we will restore the volume to the user's volume.
+ mediaPlayer.volume = mediaPlayer.volume * 0.5f
+ }
+ }
+ }
+ }
+
+ private fun unDuckActivePlayers() {
+ Threading.runOnMainThread {
+ // Resume players that were paused due to audio focus loss
+ players.forEach { player ->
+ player.player?.let { mediaPlayer ->
+ // Restore full volume if it was ducked
+ if (mediaPlayer.volume != 0f && mediaPlayer.volume.toDouble() != player.userVolume) {
+ mediaPlayer.volume = player.userVolume.toFloat()
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/VideoManager.kt b/packages/react-native-video/android/src/main/java/com/video/core/VideoManager.kt
index 979f41a4..07f7b141 100644
--- a/packages/react-native-video/android/src/main/java/com/video/core/VideoManager.kt
+++ b/packages/react-native-video/android/src/main/java/com/video/core/VideoManager.kt
@@ -2,17 +2,27 @@ package com.video.core
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
+import com.facebook.react.bridge.LifecycleEventListener
+import com.margelo.nitro.NitroModules
import com.margelo.nitro.video.HybridVideoPlayer
import com.video.view.VideoView
import java.lang.ref.WeakReference
@OptIn(UnstableApi::class)
-object VideoManager {
+object VideoManager : LifecycleEventListener {
// nitroId -> weak VideoView
private val views = mutableMapOf>()
// player -> list of nitroIds of views that are using this player
private val players = mutableMapOf>()
+ var audioFocusManager = AudioFocusManager()
+
+ init {
+ NitroModules.applicationContext?.apply {
+ addLifecycleEventListener(this@VideoManager)
+ }
+ }
+
fun maybePassPlayerToView(player: HybridVideoPlayer) {
val views = players[player]?.mapNotNull { getVideoViewWeakReferenceByNitroId(it)?.get() } ?: return
val latestView = views.lastOrNull() ?: return
@@ -59,10 +69,13 @@ object VideoManager {
if (!players.containsKey(player)) {
players[player] = mutableListOf()
}
+
+ audioFocusManager.registerPlayer(player)
}
fun unregisterPlayer(player: HybridVideoPlayer) {
players.remove(player)
+ audioFocusManager.unregisterPlayer(player)
}
fun getPlayerByNitroId(nitroId: Int): HybridVideoPlayer? {
@@ -75,7 +88,7 @@ object VideoManager {
// Remove old mapping
if (oldNitroId != -1) {
views.remove(oldNitroId)
-
+
// Update player mappings
players.keys.forEach { player ->
players[player]?.let { nitroIds ->
@@ -85,7 +98,7 @@ object VideoManager {
}
}
}
-
+
// Add new mapping
views[newNitroId] = WeakReference(view)
}
@@ -93,4 +106,32 @@ object VideoManager {
fun getVideoViewWeakReferenceByNitroId(nitroId: Int): WeakReference? {
return views[nitroId]
}
+
+ // ------------ Lifecycle Handler ------------
+ private fun onAppEnterForeground() {
+ players.keys.forEach { player ->
+ if (player.wasAutoPaused) {
+ player.play()
+ }
+ }
+ }
+
+ private fun onAppEnterBackground() {
+ players.keys.forEach { player ->
+ if (!player.playInBackground && player.isPlaying) {
+ player.wasAutoPaused = player.isPlaying
+ player.pause()
+ }
+ }
+ }
+
+ override fun onHostResume() {
+ onAppEnterForeground()
+ }
+
+ override fun onHostPause() {
+ onAppEnterBackground()
+ }
+
+ override fun onHostDestroy() {}
}
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/extensions/ResizeMode+AspectRatioFrameLayout.kt b/packages/react-native-video/android/src/main/java/com/video/core/extensions/ResizeMode+AspectRatioFrameLayout.kt
new file mode 100644
index 00000000..45289565
--- /dev/null
+++ b/packages/react-native-video/android/src/main/java/com/video/core/extensions/ResizeMode+AspectRatioFrameLayout.kt
@@ -0,0 +1,20 @@
+package com.video.core.extensions
+
+import androidx.annotation.OptIn
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.ui.AspectRatioFrameLayout
+import com.margelo.nitro.video.ResizeMode
+
+@OptIn(UnstableApi::class)
+/**
+ * Converts a [ResizeMode] to a [AspectRatioFrameLayout] resize mode.
+ * @return The corresponding [AspectRatioFrameLayout] resize mode.
+ */
+fun ResizeMode.toAspectRatioFrameLayout(): Int {
+ return when (this) {
+ ResizeMode.CONTAIN -> AspectRatioFrameLayout.RESIZE_MODE_FIT
+ ResizeMode.COVER -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
+ ResizeMode.STRETCH -> AspectRatioFrameLayout.RESIZE_MODE_FILL
+ ResizeMode.NONE -> AspectRatioFrameLayout.RESIZE_MODE_FIT
+ }
+}
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/extensions/SubtitleType+toString.kt b/packages/react-native-video/android/src/main/java/com/video/core/extensions/SubtitleType+toString.kt
new file mode 100644
index 00000000..e18d0616
--- /dev/null
+++ b/packages/react-native-video/android/src/main/java/com/video/core/extensions/SubtitleType+toString.kt
@@ -0,0 +1,14 @@
+package com.video.core.extensions
+
+import com.margelo.nitro.video.SubtitleType
+
+fun SubtitleType.toStringExtension(): String {
+ return when {
+ this == SubtitleType.AUTO -> "auto"
+ this == SubtitleType.VTT -> "vtt"
+ this == SubtitleType.SRT -> "srt"
+ this == SubtitleType.SSA -> "ssa"
+ this == SubtitleType.ASS -> "ass"
+ else -> throw IllegalArgumentException("Unknown SubtitleType: $this")
+ }
+}
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/extensions/VideoPlaybackService+ServiceManagment.kt b/packages/react-native-video/android/src/main/java/com/video/core/extensions/VideoPlaybackService+ServiceManagment.kt
new file mode 100644
index 00000000..24c045a8
--- /dev/null
+++ b/packages/react-native-video/android/src/main/java/com/video/core/extensions/VideoPlaybackService+ServiceManagment.kt
@@ -0,0 +1,48 @@
+package com.video.core.extensions
+
+import android.content.Context
+import android.content.Context.BIND_AUTO_CREATE
+import android.content.Context.BIND_INCLUDE_CAPABILITIES
+import android.content.Intent
+import android.os.Build
+import androidx.annotation.OptIn
+import androidx.media3.common.util.UnstableApi
+import com.margelo.nitro.NitroModules
+import com.margelo.nitro.video.HybridVideoPlayer
+import com.video.core.services.playback.VideoPlaybackService
+import com.video.core.services.playback.VideoPlaybackServiceConnection
+
+fun VideoPlaybackService.Companion.startService(
+ context: Context,
+ serviceConnection: VideoPlaybackServiceConnection
+) {
+ val reactContext = NitroModules.applicationContext ?: return
+
+ val intent = Intent(context, VideoPlaybackService::class.java)
+ intent.action = VIDEO_PLAYBACK_SERVICE_INTERFACE
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ reactContext.startForegroundService(intent);
+ } else {
+ reactContext.startService(intent);
+ }
+
+ val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ BIND_AUTO_CREATE or BIND_INCLUDE_CAPABILITIES
+ } else {
+ BIND_AUTO_CREATE
+ }
+
+ context.bindService(intent, serviceConnection, flags)
+}
+
+@OptIn(UnstableApi::class)
+fun VideoPlaybackService.Companion.stopService(
+ player: HybridVideoPlayer,
+ serviceConnection: VideoPlaybackServiceConnection
+) {
+ try {
+ NitroModules.applicationContext?.currentActivity?.unbindService(serviceConnection)
+ serviceConnection.unregisterPlayer(player)
+ } catch (_: Exception) {}
+}
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/player/MediaItemUtils.kt b/packages/react-native-video/android/src/main/java/com/video/core/player/MediaItemUtils.kt
index 0f1483b9..12341cd5 100644
--- a/packages/react-native-video/android/src/main/java/com/video/core/player/MediaItemUtils.kt
+++ b/packages/react-native-video/android/src/main/java/com/video/core/player/MediaItemUtils.kt
@@ -9,7 +9,9 @@ import androidx.media3.common.C
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
import com.margelo.nitro.video.NativeVideoConfig
+import com.margelo.nitro.video.SubtitleType
import com.video.core.SourceError
+import com.video.core.extensions.toStringExtension
private const val TAG = "MediaItemUtils"
@@ -32,7 +34,11 @@ fun getSubtitlesConfiguration(
if (config.externalSubtitles != null) {
for (subtitle in config.externalSubtitles) {
- val ext = MimeTypeMap.getFileExtensionFromUrl(subtitle.uri)
+ val ext = if (subtitle.type == SubtitleType.AUTO) {
+ MimeTypeMap.getFileExtensionFromUrl(subtitle.uri)
+ } else {
+ subtitle.type.toStringExtension()
+ }
val mimeType = when (ext?.lowercase()) {
"srt" -> MimeTypes.APPLICATION_SUBRIP
@@ -48,7 +54,7 @@ fun getSubtitlesConfiguration(
val subtitleConfig = MediaItem.SubtitleConfiguration.Builder(subtitle.uri.toUri())
.setId("external-subtitle-${subtitle.uri}")
.setMimeType(mimeType)
- .setSelectionFlags(C.SELECTION_FLAG_DEFAULT or C.SELECTION_FLAG_AUTOSELECT)
+ .setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setRoleFlags(C.ROLE_FLAG_SUBTITLE)
.setLabel(subtitle.label)
.build()
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackCallback.kt b/packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackCallback.kt
new file mode 100644
index 00000000..a041d42a
--- /dev/null
+++ b/packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackCallback.kt
@@ -0,0 +1,58 @@
+package com.video.core.services.playback
+
+import android.os.Bundle
+import androidx.annotation.OptIn
+import androidx.media3.common.Player
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.session.MediaSession
+import androidx.media3.session.SessionCommand
+import androidx.media3.session.SessionResult
+import com.google.common.util.concurrent.ListenableFuture
+
+@OptIn(UnstableApi::class)
+class VideoPlaybackCallback : MediaSession.Callback {
+
+ override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
+ try {
+ return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
+ .setAvailablePlayerCommands(
+ MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
+ .add(Player.COMMAND_SEEK_FORWARD)
+ .add(Player.COMMAND_SEEK_BACK)
+ .build()
+ ).setAvailableSessionCommands(
+ MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
+ .add(
+ SessionCommand(
+ VideoPlaybackService.Companion.COMMAND.SEEK_FORWARD.stringValue,
+ Bundle.EMPTY
+ )
+ )
+ .add(
+ SessionCommand(
+ VideoPlaybackService.Companion.COMMAND.SEEK_BACKWARD.stringValue,
+ Bundle.EMPTY
+ )
+ )
+ .build()
+ )
+ .build()
+ } catch (e: Exception) {
+ return MediaSession.ConnectionResult.reject()
+ }
+ }
+
+ override fun onCustomCommand(
+ session: MediaSession,
+ controller: MediaSession.ControllerInfo,
+ customCommand: SessionCommand,
+ args: Bundle
+ ): ListenableFuture {
+ VideoPlaybackService.Companion.handleCommand(
+ VideoPlaybackService.Companion.commandFromString(
+ customCommand.customAction
+ ), session
+ )
+ return super.onCustomCommand(session, controller, customCommand, args)
+ }
+}
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackService.kt b/packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackService.kt
new file mode 100644
index 00000000..e3b0b6a4
--- /dev/null
+++ b/packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackService.kt
@@ -0,0 +1,327 @@
+package com.video.core.services.playback
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.Binder
+import android.os.Build
+import android.os.Bundle
+import android.os.IBinder
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.core.app.NotificationCompat
+import androidx.lifecycle.OnLifecycleEvent
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.CommandButton
+import androidx.media3.session.MediaSession
+import androidx.media3.session.MediaSessionService
+import androidx.media3.session.MediaStyleNotificationHelper
+import androidx.media3.session.SessionCommand
+import androidx.media3.ui.R
+import com.margelo.nitro.video.HybridVideoPlayer
+import okhttp3.internal.immutableListOf
+
+class VideoPlaybackServiceBinder(val service: VideoPlaybackService): Binder()
+
+@OptIn(UnstableApi::class)
+class VideoPlaybackService : MediaSessionService() {
+ private var mediaSessionsList = mutableMapOf()
+ private var binder = VideoPlaybackServiceBinder(this)
+ private var sourceActivity: Class? = null
+ private var placeholderCanceled = false
+
+ // Controls for Android 13+ - see buildNotification function
+ private val commandSeekForward = SessionCommand(COMMAND.SEEK_FORWARD.stringValue, Bundle.EMPTY)
+ private val commandSeekBackward = SessionCommand(COMMAND.SEEK_BACKWARD.stringValue, Bundle.EMPTY)
+
+ @SuppressLint("PrivateResource")
+ private val seekForwardBtn = CommandButton.Builder()
+ .setDisplayName("forward")
+ .setSessionCommand(commandSeekForward)
+ .setIconResId(R.drawable.exo_notification_fastforward)
+ .build()
+
+ @SuppressLint("PrivateResource")
+ private val seekBackwardBtn = CommandButton.Builder()
+ .setDisplayName("backward")
+ .setSessionCommand(commandSeekBackward)
+ .setIconResId(R.drawable.exo_notification_rewind)
+ .build()
+
+ // Player Registry
+ fun registerPlayer(player: HybridVideoPlayer, from: Class) {
+ if (mediaSessionsList.containsKey(player)) {
+ return
+ }
+ sourceActivity = from
+
+ val mediaSession = MediaSession.Builder(this, player.playerPointer)
+ .setId("RNVideoPlaybackService_" + player.hashCode())
+ .setCallback(VideoPlaybackCallback())
+ .setCustomLayout(immutableListOf(seekBackwardBtn, seekForwardBtn))
+ .build()
+
+ mediaSessionsList[player] = mediaSession
+ addSession(mediaSession)
+
+ // Manually trigger initial notification creation for the registered player
+ // This ensures the player notification appears immediately, even if not playing
+ onUpdateNotification(mediaSession, true)
+ }
+
+ fun unregisterPlayer(player: HybridVideoPlayer) {
+ hidePlayerNotification(player.playerPointer)
+ val session = mediaSessionsList.remove(player)
+ session?.release()
+ if (mediaSessionsList.isEmpty()) {
+ cleanup()
+ stopSelf()
+ }
+ }
+
+ // Callbacks
+
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = null
+
+ override fun onBind(intent: Intent?): IBinder {
+ super.onBind(intent)
+ return binder
+ }
+
+ override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
+ val notification = buildNotification(session)
+ val notificationId = session.player.hashCode()
+ val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+
+ // Always cancel the placeholder notification once we have a real player notification
+ if (!placeholderCanceled) {
+ notificationManager.cancel(PLACEHOLDER_NOTIFICATION_ID)
+ placeholderCanceled = true
+ }
+
+ if (startInForegroundRequired) {
+ startForeground(notificationId, notification)
+ } else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ notificationManager.createNotificationChannel(
+ NotificationChannel(
+ NOTIFICATION_CHANEL_ID,
+ NOTIFICATION_CHANEL_ID,
+ NotificationManager.IMPORTANCE_LOW
+ )
+ )
+ }
+
+ if (session.player.currentMediaItem == null) {
+ notificationManager.cancel(notificationId)
+ return
+ }
+
+ notificationManager.notify(notificationId, notification)
+ }
+ }
+
+ override fun onTaskRemoved(rootIntent: Intent?) {
+ cleanup()
+ stopSelf()
+ }
+
+ override fun onDestroy() {
+ cleanup()
+ val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ notificationManager.deleteNotificationChannel(NOTIFICATION_CHANEL_ID)
+ }
+ super.onDestroy()
+ }
+
+ private fun buildNotification(session: MediaSession): Notification {
+ val returnToPlayer = Intent(this, sourceActivity ?: this.javaClass).apply {
+ flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ }
+
+ /*
+ * On Android 13+ controls are automatically handled via media session
+ * On Android 12 and bellow we need to add controls manually
+ */
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
+ .setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
+ .setStyle(MediaStyleNotificationHelper.MediaStyle(session))
+ .setContentIntent(PendingIntent.getActivity(this, 0, returnToPlayer, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
+ .build()
+ } else {
+ val playerId = session.player.hashCode()
+
+ // Action for COMMAND.SEEK_BACKWARD
+ val seekBackwardIntent = Intent(this, VideoPlaybackService::class.java).apply {
+ putExtra("PLAYER_ID", playerId)
+ putExtra("ACTION", COMMAND.SEEK_BACKWARD.stringValue)
+ }
+ val seekBackwardPendingIntent = PendingIntent.getService(
+ this,
+ playerId * 10,
+ seekBackwardIntent,
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ // ACTION FOR COMMAND.TOGGLE_PLAY
+ val togglePlayIntent = Intent(this, VideoPlaybackService::class.java).apply {
+ putExtra("PLAYER_ID", playerId)
+ putExtra("ACTION", COMMAND.TOGGLE_PLAY.stringValue)
+ }
+ val togglePlayPendingIntent = PendingIntent.getService(
+ this,
+ playerId * 10 + 1,
+ togglePlayIntent,
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ // ACTION FOR COMMAND.SEEK_FORWARD
+ val seekForwardIntent = Intent(this, VideoPlaybackService::class.java).apply {
+ putExtra("PLAYER_ID", playerId)
+ putExtra("ACTION", COMMAND.SEEK_FORWARD.stringValue)
+ }
+ val seekForwardPendingIntent = PendingIntent.getService(
+ this,
+ playerId * 10 + 2,
+ seekForwardIntent,
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
+ // Show controls on lock screen even when user hides sensitive content.
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
+ // Add media control buttons that invoke intents in your media service
+ .addAction(androidx.media3.session.R.drawable.media3_icon_rewind, "Seek Backward", seekBackwardPendingIntent) // #0
+ .addAction(
+ if (session.player.isPlaying) {
+ androidx.media3.session.R.drawable.media3_icon_pause
+ } else {
+ androidx.media3.session.R.drawable.media3_icon_play
+ },
+ "Toggle Play",
+ togglePlayPendingIntent
+ ) // #1
+ .addAction(androidx.media3.session.R.drawable.media3_icon_fast_forward, "Seek Forward", seekForwardPendingIntent) // #2
+ // Apply the media style template
+ .setStyle(MediaStyleNotificationHelper.MediaStyle(session).setShowActionsInCompactView(0, 1, 2))
+ .setContentTitle(session.player.mediaMetadata.title)
+ .setContentText(session.player.mediaMetadata.description)
+ .setContentIntent(PendingIntent.getActivity(this, 0, returnToPlayer, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
+ .setLargeIcon(session.player.mediaMetadata.artworkUri?.let { session.bitmapLoader.loadBitmap(it).get() })
+ .setOngoing(true)
+ .build()
+ }
+ }
+
+ private fun hidePlayerNotification(player: ExoPlayer) {
+ val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancel(player.hashCode())
+ }
+
+ private fun hideAllNotifications() {
+ val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancelAll()
+ }
+
+ private fun cleanup() {
+ hideAllNotifications()
+ mediaSessionsList.forEach { (_, session) ->
+ session.release()
+ }
+ mediaSessionsList.clear()
+ placeholderCanceled = false
+ }
+
+ private fun createPlaceholderNotification(): Notification {
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ notificationManager.createNotificationChannel(
+ NotificationChannel(
+ NOTIFICATION_CHANEL_ID,
+ NOTIFICATION_CHANEL_ID,
+ NotificationManager.IMPORTANCE_LOW
+ )
+ )
+ }
+
+ return NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
+ .setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
+ .setContentTitle("Media playback")
+ .setContentText("Preparing playback")
+ .build()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !placeholderCanceled) {
+ startForeground(PLACEHOLDER_NOTIFICATION_ID, createPlaceholderNotification())
+ }
+
+ intent?.let {
+ val playerId = it.getIntExtra("PLAYER_ID", -1)
+ val actionCommand = it.getStringExtra("ACTION")
+
+ if (playerId < 0) {
+ Log.w(TAG, "Received Command without playerId")
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ if (actionCommand == null) {
+ Log.w(TAG, "Received Command without action command")
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ val session = mediaSessionsList.values.find { s -> s.player.hashCode() == playerId } ?: return super.onStartCommand(intent, flags, startId)
+
+ handleCommand(commandFromString(actionCommand), session)
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ companion object {
+ private const val SEEK_INTERVAL_MS = 10000L
+ private const val TAG = "VideoPlaybackService"
+ private const val PLACEHOLDER_NOTIFICATION_ID = 9999
+
+ const val NOTIFICATION_CHANEL_ID = "RNVIDEO_SESSION_NOTIFICATION"
+ const val VIDEO_PLAYBACK_SERVICE_INTERFACE = SERVICE_INTERFACE
+
+ enum class COMMAND(val stringValue: String) {
+ NONE("NONE"),
+ SEEK_FORWARD("COMMAND_SEEK_FORWARD"),
+ SEEK_BACKWARD("COMMAND_SEEK_BACKWARD"),
+ TOGGLE_PLAY("COMMAND_TOGGLE_PLAY"),
+ PLAY("COMMAND_PLAY"),
+ PAUSE("COMMAND_PAUSE")
+ }
+
+ fun commandFromString(value: String): COMMAND =
+ when (value) {
+ COMMAND.SEEK_FORWARD.stringValue -> COMMAND.SEEK_FORWARD
+ COMMAND.SEEK_BACKWARD.stringValue -> COMMAND.SEEK_BACKWARD
+ COMMAND.TOGGLE_PLAY.stringValue -> COMMAND.TOGGLE_PLAY
+ COMMAND.PLAY.stringValue -> COMMAND.PLAY
+ COMMAND.PAUSE.stringValue -> COMMAND.PAUSE
+ else -> COMMAND.NONE
+ }
+ fun handleCommand(command: COMMAND, session: MediaSession) {
+ // TODO: get somehow ControlsConfig here - for now hardcoded 10000ms
+
+ when (command) {
+ COMMAND.SEEK_BACKWARD -> session.player.seekTo(session.player.contentPosition - SEEK_INTERVAL_MS)
+ COMMAND.SEEK_FORWARD -> session.player.seekTo(session.player.contentPosition + SEEK_INTERVAL_MS)
+ COMMAND.TOGGLE_PLAY -> handleCommand(if (session.player.isPlaying) COMMAND.PAUSE else COMMAND.PLAY, session)
+ COMMAND.PLAY -> session.player.play()
+ COMMAND.PAUSE -> session.player.pause()
+ else -> Log.w(TAG, "Received COMMAND.NONE - was there an error?")
+ }
+ }
+ }
+}
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackServiceConnection.kt b/packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackServiceConnection.kt
new file mode 100644
index 00000000..87486376
--- /dev/null
+++ b/packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackServiceConnection.kt
@@ -0,0 +1,56 @@
+package com.video.core.services.playback
+
+import android.content.ComponentName
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.media3.common.util.UnstableApi
+import com.margelo.nitro.NitroModules
+import com.margelo.nitro.video.HybridVideoPlayer
+import java.lang.ref.WeakReference
+
+@OptIn(UnstableApi::class)
+class VideoPlaybackServiceConnection (private val player: WeakReference) :
+ ServiceConnection {
+ var serviceBinder: VideoPlaybackServiceBinder? = null
+
+ override fun onServiceConnected(componentName: ComponentName?, binder: IBinder?) {
+ val player = player.get() ?: return
+
+ try {
+ val activity = NitroModules.Companion.applicationContext?.currentActivity ?: run {
+ Log.e("VideoPlaybackServiceConnection", "Activity is null")
+ return
+ }
+
+ serviceBinder = binder as? VideoPlaybackServiceBinder
+ serviceBinder?.service?.registerPlayer(player, activity.javaClass)
+ } catch (err: Exception) {
+ Log.e("VideoPlaybackServiceConnection", "Could not bind to playback service", err)
+ }
+ }
+
+ override fun onServiceDisconnected(componentName: ComponentName?) {
+ player.get()?.let {
+ unregisterPlayer(it)
+ }
+ serviceBinder = null
+ }
+
+ override fun onNullBinding(componentName: ComponentName?) {
+ Log.e(
+ "VideoPlaybackServiceConnection",
+ "Could not bind to playback service - there can be issues with background playback" +
+ "and notification controls"
+ )
+ }
+
+ fun unregisterPlayer(player: HybridVideoPlayer) {
+ try {
+ if (serviceBinder?.service != null) {
+ serviceBinder?.service?.unregisterPlayer(player)
+ }
+ } catch (_: Exception) {}
+ }
+}
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/utils/PictureInPictureUtils.kt b/packages/react-native-video/android/src/main/java/com/video/core/utils/PictureInPictureUtils.kt
index d227c2f0..1e7f0a8c 100644
--- a/packages/react-native-video/android/src/main/java/com/video/core/utils/PictureInPictureUtils.kt
+++ b/packages/react-native-video/android/src/main/java/com/video/core/utils/PictureInPictureUtils.kt
@@ -25,7 +25,7 @@ object PictureInPictureUtils {
fun createPictureInPictureParams(videoView: VideoView): PictureInPictureParams {
val builder = PictureInPictureParams.Builder()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && videoView.autoEnterPictureInPicture) {
builder.setAutoEnterEnabled(videoView.autoEnterPictureInPicture)
}
@@ -35,6 +35,19 @@ object PictureInPictureUtils {
.build()
}
+ @RequiresApi(Build.VERSION_CODES.O)
+ fun createDisabledPictureInPictureParams(videoView: VideoView): PictureInPictureParams {
+ val defaultParams = PictureInPictureParams.Builder()
+ .setAspectRatio(null) // Clear aspect ratio
+ .setSourceRectHint(null) // Clear source rect hint
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ defaultParams.setAutoEnterEnabled(false)
+ }
+
+ return defaultParams.build()
+ }
+
fun calculateAspectRatio(view: View): Rational {
// AspectRatio for PIP must be between 2.39:1 and 1:2.39
// see: https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/utils/TextTrackUtils.kt b/packages/react-native-video/android/src/main/java/com/video/core/utils/TextTrackUtils.kt
new file mode 100644
index 00000000..c48e2319
--- /dev/null
+++ b/packages/react-native-video/android/src/main/java/com/video/core/utils/TextTrackUtils.kt
@@ -0,0 +1,164 @@
+package com.video.core.utils
+
+import androidx.media3.common.C
+import androidx.media3.common.TrackSelectionOverride
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.ExoPlayer
+import com.margelo.nitro.video.HybridVideoPlayerSourceSpec
+import com.margelo.nitro.video.TextTrack
+
+@UnstableApi
+object TextTrackUtils {
+ fun getAvailableTextTracks(player: ExoPlayer, source: HybridVideoPlayerSourceSpec): Array {
+ return Threading.runOnMainThreadSync {
+ val tracks = mutableListOf()
+ val currentTracks = player.currentTracks
+
+ // Get all text tracks from the current player tracks (includes both built-in and external)
+ for (trackGroup in currentTracks.groups) {
+ if (trackGroup.type == C.TRACK_TYPE_TEXT) {
+ for (trackIndex in 0 until trackGroup.length) {
+ val format = trackGroup.getTrackFormat(trackIndex)
+ val trackId = format.id ?: "text-$trackIndex"
+ val label = format.label ?: "Unknown ${trackIndex + 1}"
+ val language = format.language
+ val isSelected = trackGroup.isTrackSelected(trackIndex)
+
+ // Determine if this is an external track by checking if it matches external subtitle labels
+ val isExternal = source.config.externalSubtitles?.any { subtitle ->
+ label.contains(subtitle.label, ignoreCase = true)
+ } ?: false
+
+ val finalTrackId = if (isExternal) "external-$trackIndex" else trackId
+
+ tracks.add(
+ TextTrack(
+ id = finalTrackId,
+ label = label,
+ language = language,
+ selected = isSelected
+ )
+ )
+ }
+ }
+ }
+
+ tracks.toTypedArray()
+ }
+ }
+
+ fun selectTextTrack(
+ player: ExoPlayer,
+ textTrack: TextTrack?,
+ source: HybridVideoPlayerSourceSpec,
+ onTrackChange: (TextTrack?) -> Unit,
+ ): Int? {
+ return Threading.runOnMainThreadSync {
+ val trackSelector = player.trackSelectionParameters.buildUpon()
+
+ // If textTrack is null, disable all text tracks
+ if (textTrack == null) {
+ trackSelector.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
+ player.trackSelectionParameters = trackSelector.build()
+ onTrackChange(null)
+ return@runOnMainThreadSync null
+ }
+
+ if (textTrack.id.isEmpty()) {
+ // Disable all text tracks
+ trackSelector.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
+ player.trackSelectionParameters = trackSelector.build()
+ onTrackChange(null)
+ return@runOnMainThreadSync null
+ }
+
+ val currentTracks = player.currentTracks
+ var trackFound = false
+ var selectedExternalTrackIndex: Int? = null
+
+ // Find and select the specific text track
+ for (trackGroup in currentTracks.groups) {
+ if (trackGroup.type == C.TRACK_TYPE_TEXT) {
+ for (trackIndex in 0 until trackGroup.length) {
+ val format = trackGroup.getTrackFormat(trackIndex)
+ val currentTrackId = format.id ?: "text-$trackIndex"
+ val label = format.label ?: "Unknown ${trackIndex + 1}"
+
+ // Check if this matches our target track (either by original ID or by external ID)
+ val isExternal = source.config.externalSubtitles?.any { subtitle ->
+ label.contains(subtitle.label, ignoreCase = true)
+ } == true
+
+ val finalTrackId =
+ if (isExternal) "external-$trackIndex" else currentTrackId
+
+ if (finalTrackId == textTrack.id) {
+ // Enable this specific track
+ trackSelector.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
+ trackSelector.setOverrideForType(
+ TrackSelectionOverride(
+ trackGroup.mediaTrackGroup,
+ listOf(trackIndex)
+ )
+ )
+
+ // Update selection state
+ selectedExternalTrackIndex = if (isExternal) {
+ trackIndex
+ } else {
+ null
+ }
+
+ onTrackChange(textTrack)
+ trackFound = true
+ break
+ }
+ }
+ if (trackFound) {
+ break
+ }
+ }
+ }
+
+ // Apply the track selection parameters regardless of whether we found a track
+ player.trackSelectionParameters = trackSelector.build()
+ selectedExternalTrackIndex
+ }
+ }
+
+ fun getSelectedTrack(player: ExoPlayer, source: HybridVideoPlayerSourceSpec): TextTrack? {
+ return Threading.runOnMainThreadSync {
+ val currentTracks = player.currentTracks
+
+ // Find the currently selected text track
+ for (trackGroup in currentTracks.groups) {
+ if (trackGroup.type == C.TRACK_TYPE_TEXT && trackGroup.isSelected) {
+ for (trackIndex in 0 until trackGroup.length) {
+ if (trackGroup.isTrackSelected(trackIndex)) {
+ val format = trackGroup.getTrackFormat(trackIndex)
+ val trackId = format.id ?: "text-$trackIndex"
+ val label = format.label ?: "Unknown ${trackIndex + 1}"
+ val language = format.language
+
+ // Determine if this is an external track by checking if it matches external subtitle labels
+ val isExternal = source.config.externalSubtitles?.any { subtitle ->
+ label.contains(subtitle.label, ignoreCase = true)
+ } == true
+
+ val finalTrackId = if (isExternal) "external-$trackIndex" else trackId
+
+ return@runOnMainThreadSync TextTrack(
+ id = finalTrackId,
+ label = label,
+ language = language,
+ selected = true
+ )
+ }
+ }
+ }
+ }
+
+ null
+ }
+ }
+}
diff --git a/packages/react-native-video/android/src/main/java/com/video/core/utils/Threading.kt b/packages/react-native-video/android/src/main/java/com/video/core/utils/Threading.kt
index 84476caa..56c1b0a2 100644
--- a/packages/react-native-video/android/src/main/java/com/video/core/utils/Threading.kt
+++ b/packages/react-native-video/android/src/main/java/com/video/core/utils/Threading.kt
@@ -6,6 +6,7 @@ import com.margelo.nitro.NitroModules
import com.video.core.LibraryError
import java.util.concurrent.Callable
import java.util.concurrent.FutureTask
+import kotlin.reflect.KProperty
object Threading {
@JvmStatic
@@ -39,4 +40,36 @@ object Threading {
futureTask.get()
}
}
+
+ class MainThreadProperty(
+ private val get: Reference.() -> Type,
+ private val set: (Reference.(Type) -> Unit)? = null
+ ) {
+ operator fun getValue(thisRef: Reference, property: KProperty<*>): Type {
+ return runOnMainThreadSync { thisRef.get() }
+ }
+
+ operator fun setValue(thisRef: Reference, property: KProperty<*>, value: Type) {
+ val setter = set ?: throw IllegalStateException("Property ${property.name} is read-only")
+ runOnMainThread { thisRef.setter(value) }
+ }
+ }
+
+ /**
+ * Read-only property that runs on main thread
+ * @param get The getter function that runs synchronously on the main thread.
+ *
+ * @throws [IllegalStateException] if there will be a write operation
+ */
+ fun Reference.mainThreadProperty(get: Reference.() -> T) = MainThreadProperty(get)
+
+ /**
+ * Read-only property that runs on main thread
+ * @param get The getter function that runs synchronously on the main thread
+ * @param set The setter function that runs asynchronously on the main thread
+ */
+ fun Reference.mainThreadProperty(
+ get: Reference.() -> T,
+ set: Reference.(T) -> Unit
+ ) = MainThreadProperty(get, set)
}
diff --git a/packages/react-native-video/android/src/main/java/com/video/hybrids/videoplayer/HybridVideoPlayer.kt b/packages/react-native-video/android/src/main/java/com/video/hybrids/videoplayer/HybridVideoPlayer.kt
index cb346a74..ca5fca36 100644
--- a/packages/react-native-video/android/src/main/java/com/video/hybrids/videoplayer/HybridVideoPlayer.kt
+++ b/packages/react-native-video/android/src/main/java/com/video/hybrids/videoplayer/HybridVideoPlayer.kt
@@ -1,5 +1,6 @@
package com.margelo.nitro.video
+import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
@@ -8,15 +9,14 @@ import androidx.media3.common.Metadata
import androidx.media3.common.PlaybackException
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
-import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.Tracks
import androidx.media3.common.text.CueGroup
+import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.AnalyticsListener
-import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.exoplayer.upstream.DefaultAllocator
import androidx.media3.extractor.metadata.emsg.EventMessage
import androidx.media3.extractor.metadata.id3.Id3Frame
@@ -30,12 +30,18 @@ import com.video.core.PlayerError
import com.video.core.VideoManager
import com.video.core.player.OnAudioFocusChangedListener
import com.video.core.recivers.AudioBecomingNoisyReceiver
+import com.video.core.services.playback.VideoPlaybackService
+import com.video.core.utils.Threading.mainThreadProperty
import com.video.core.utils.Threading.runOnMainThread
import com.video.core.utils.Threading.runOnMainThreadSync
import com.video.core.utils.VideoOrientationUtils
import com.video.view.VideoView
import java.lang.ref.WeakReference
import kotlin.math.max
+import com.video.core.extensions.startService
+import com.video.core.extensions.stopService
+import com.video.core.services.playback.VideoPlaybackServiceConnection
+import com.video.core.utils.TextTrackUtils
@UnstableApi
@DoNotStrip
@@ -51,10 +57,18 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
}
private var allocator: DefaultAllocator? = null
+ private var context: Context = NitroModules.applicationContext
+ ?.currentActivity
+ ?.applicationContext
+ ?: run {
+ throw LibraryError.ApplicationContextNotFound
+ }
- private var player: ExoPlayer? = null
+ var player: ExoPlayer? = null
private var currentPlayerView: WeakReference? = null
+ var wasAutoPaused = false
+
// Time updates
private val progressHandler = Handler(Looper.getMainLooper())
private var progressRunnable: Runnable? = null
@@ -63,6 +77,12 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
private val audioFocusChangedListener = OnAudioFocusChangedListener()
private val audioBecomingNoisyReceiver = AudioBecomingNoisyReceiver()
+ // Service Connection
+ private val videoPlaybackServiceConnection = VideoPlaybackServiceConnection(WeakReference(this))
+
+ // Text track selection state
+ private var selectedExternalTrackIndex: Int? = null
+
private companion object {
const val PROGRESS_UPDATE_INTERVAL_MS = 250L
private const val TAG = "HybridVideoPlayer"
@@ -103,42 +123,44 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
}
// Player Properties
- override var currentTime: Double
- get() = runOnMainThreadSync { return@runOnMainThreadSync playerPointer.currentPosition.toDouble() / 1000.0 }
- set(value) = runOnMainThread { playerPointer.seekTo((value * 1000).toLong()) }
+ override var currentTime: Double by mainThreadProperty(
+ get = { playerPointer.currentPosition.toDouble() / 1000.0 },
+ set = { value -> runOnMainThread { playerPointer.seekTo((value * 1000).toLong()) } }
+ )
// volume defined by user
var userVolume: Double = 1.0
- override var volume: Double
- get() = runOnMainThreadSync { return@runOnMainThreadSync playerPointer.volume.toDouble() }
- set(value) = runOnMainThread {
+ override var volume: Double by mainThreadProperty(
+ get = { playerPointer.volume.toDouble() },
+ set = { value ->
userVolume = value
playerPointer.volume = value.toFloat()
}
+ )
- override val duration: Double
- get() {
- val duration = runOnMainThreadSync { return@runOnMainThreadSync playerPointer.duration }
- return if (duration == C.TIME_UNSET) Double.NaN else duration.toDouble() / 1000.0
+ override val duration: Double by mainThreadProperty(
+ get = {
+ val duration = playerPointer.duration
+ return@mainThreadProperty if (duration == C.TIME_UNSET) Double.NaN else duration.toDouble() / 1000.0
}
+ )
- override var loop: Boolean
- get() {
- val repeatMode = runOnMainThreadSync { return@runOnMainThreadSync playerPointer.repeatMode }
- return repeatMode == Player.REPEAT_MODE_ONE
- }
- set(value) = runOnMainThread {
+ override var loop: Boolean by mainThreadProperty(
+ get = {
+ playerPointer.repeatMode == Player.REPEAT_MODE_ONE
+ },
+ set = { value ->
playerPointer.repeatMode = if (value) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF
}
+ )
- override var muted: Boolean
- get() = runOnMainThreadSync {
+ override var muted: Boolean by mainThreadProperty(
+ get = {
val playerVolume = playerPointer.volume.toDouble()
- val isMuted = playerVolume == 0.0
- return@runOnMainThreadSync isMuted
- }
- set(value) = runOnMainThread {
+ return@mainThreadProperty playerVolume == 0.0
+ },
+ set = { value ->
if (value) {
userVolume = volume
playerPointer.volume = 0f
@@ -146,16 +168,43 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
playerPointer.volume = userVolume.toFloat()
}
}
+ )
- override var rate: Double
- get() = runOnMainThreadSync { return@runOnMainThreadSync playerPointer.playbackParameters.speed.toDouble() }
- set(value) = runOnMainThread {
+ override var rate: Double by mainThreadProperty(
+ get = { playerPointer.playbackParameters.speed.toDouble() },
+ set = { value ->
playerPointer.playbackParameters = playerPointer.playbackParameters.withSpeed(value.toFloat())
}
+ )
- override var isPlaying: Boolean = runOnMainThreadSync {
- return@runOnMainThreadSync player?.isPlaying == true
- }
+ override var mixAudioMode: MixAudioMode = MixAudioMode.AUTO
+ set(value) {
+ VideoManager.audioFocusManager.requestAudioFocusUpdate()
+ field = value
+ }
+
+ // iOS only property
+ override var ignoreSilentSwitchMode: IgnoreSilentSwitchMode = IgnoreSilentSwitchMode.AUTO
+
+ override var playInBackground: Boolean = false
+ set(value) {
+ // playback in background was disabled and is now enabled
+ if (value == true && field == false) {
+ VideoPlaybackService.startService(context, videoPlaybackServiceConnection)
+ field = true
+ }
+ // playback in background was enabled and is now disabled
+ else if (field == true) {
+ VideoPlaybackService.stopService(this, videoPlaybackServiceConnection)
+ field = false
+ }
+ }
+
+ override var playWhenInactive: Boolean = false
+
+ override var isPlaying: Boolean by mainThreadProperty(
+ get = { player?.isPlaying == true }
+ )
private fun initializePlayer() {
if (NitroModules.applicationContext == null) {
@@ -264,6 +313,10 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
}
private fun release() {
+ if (playInBackground) {
+ VideoPlaybackService.stopService(this, videoPlaybackServiceConnection)
+ }
+
VideoManager.unregisterPlayer(this)
stopProgressUpdates()
runOnMainThread {
@@ -458,6 +511,7 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
this@HybridVideoPlayer.volume = volume.toDouble()
}
+ VideoManager.audioFocusManager.requestAudioFocusUpdate()
eventEmitter.onVolumeChange(volume.toDouble())
}
@@ -497,4 +551,22 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
super.onTracksChanged(tracks)
}
}
+
+ // MARK: - Text Track Management
+
+ override fun getAvailableTextTracks(): Array {
+ return TextTrackUtils.getAvailableTextTracks(playerPointer, source)
+ }
+
+ override fun selectTextTrack(textTrack: TextTrack?) {
+ selectedExternalTrackIndex = TextTrackUtils.selectTextTrack(
+ player = playerPointer,
+ textTrack = textTrack,
+ source = source,
+ onTrackChange = { track -> eventEmitter.onTrackChange(track) }
+ )
+ }
+
+ override val selectedTrack: TextTrack?
+ get() = TextTrackUtils.getSelectedTrack(playerPointer, source)
}
diff --git a/packages/react-native-video/android/src/main/java/com/video/hybrids/videoplayereventemitter/HybridVideoPlayerEventEmitter.kt b/packages/react-native-video/android/src/main/java/com/video/hybrids/videoplayereventemitter/HybridVideoPlayerEventEmitter.kt
index 7c8bf46f..64b4c7f4 100644
--- a/packages/react-native-video/android/src/main/java/com/video/hybrids/videoplayereventemitter/HybridVideoPlayerEventEmitter.kt
+++ b/packages/react-native-video/android/src/main/java/com/video/hybrids/videoplayereventemitter/HybridVideoPlayerEventEmitter.kt
@@ -33,6 +33,8 @@ class HybridVideoPlayerEventEmitter(): HybridVideoPlayerEventEmitterSpec() {
override var onTextTrackDataChanged: (Array) -> Unit = {}
+ override var onTrackChange: (TextTrack?) -> Unit = {}
+
override var onVolumeChange: (Double) -> Unit = {}
override var onStatusChange: (VideoPlayerStatus) -> Unit = {}
diff --git a/packages/react-native-video/android/src/main/java/com/video/hybrids/videoviewviewmanager/HybridVideoViewViewManager.kt b/packages/react-native-video/android/src/main/java/com/video/hybrids/videoviewviewmanager/HybridVideoViewViewManager.kt
index 3be49a1d..28e1ae4c 100644
--- a/packages/react-native-video/android/src/main/java/com/video/hybrids/videoviewviewmanager/HybridVideoViewViewManager.kt
+++ b/packages/react-native-video/android/src/main/java/com/video/hybrids/videoviewviewmanager/HybridVideoViewViewManager.kt
@@ -3,11 +3,9 @@ package com.margelo.nitro.video
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import com.facebook.proguard.annotations.DoNotStrip
-import com.video.core.LibraryError
import com.video.core.VideoManager
import com.video.core.VideoViewError
import com.video.core.utils.PictureInPictureUtils
-import com.video.view.VideoView
import com.video.core.utils.Threading
@DoNotStrip
@@ -68,6 +66,12 @@ class HybridVideoViewViewManager(nitroId: Int): HybridVideoViewViewManagerSpec()
videoView.get()?.useController = value
}
+ override var resizeMode: ResizeMode
+ get() = videoView.get()?.resizeMode ?: ResizeMode.NONE
+ set(value) {
+ videoView.get()?.resizeMode = value
+ }
+
// View callbacks
override var onPictureInPictureChange: ((Boolean) -> Unit)? = null
set(value) {
diff --git a/packages/react-native-video/android/src/main/java/com/video/view/VideoView.kt b/packages/react-native-video/android/src/main/java/com/video/view/VideoView.kt
index e5807ef9..c41c30f6 100644
--- a/packages/react-native-video/android/src/main/java/com/video/view/VideoView.kt
+++ b/packages/react-native-video/android/src/main/java/com/video/view/VideoView.kt
@@ -1,6 +1,7 @@
package com.video.view
import android.annotation.SuppressLint
+import android.app.PictureInPictureParams
import android.content.Context
import android.graphics.Color
import android.os.Build
@@ -17,6 +18,7 @@ import androidx.media3.ui.PlayerView
import com.facebook.react.bridge.ReactApplicationContext
import com.margelo.nitro.NitroModules
import com.margelo.nitro.video.HybridVideoPlayer
+import com.margelo.nitro.video.ResizeMode
import com.margelo.nitro.video.VideoViewEvents
import com.video.core.LibraryError
import com.video.core.VideoManager
@@ -26,6 +28,8 @@ import com.video.core.fragments.PictureInPictureHelperFragment
import com.video.core.utils.PictureInPictureUtils.canEnterPictureInPicture
import com.video.core.utils.PictureInPictureUtils.createPictureInPictureParams
import com.video.core.utils.Threading.runOnMainThread
+import com.video.core.extensions.toAspectRatioFrameLayout
+import com.video.core.utils.PictureInPictureUtils.createDisabledPictureInPictureParams
@UnstableApi
class VideoView @JvmOverloads constructor(
@@ -61,7 +65,7 @@ class VideoView @JvmOverloads constructor(
var autoEnterPictureInPicture: Boolean = false
set(value) {
field = value
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
val currentActivity = applicationContent.currentActivity
currentActivity?.setPictureInPictureParams(createPictureInPictureParams(this))
@@ -79,6 +83,14 @@ class VideoView @JvmOverloads constructor(
var pictureInPictureEnabled: Boolean = false
+ var resizeMode: ResizeMode = ResizeMode.NONE
+ set(value) {
+ field = value
+ runOnMainThread {
+ applyResizeMode()
+ }
+ }
+
var events = object : VideoViewEvents {
override var onPictureInPictureChange: ((Boolean) -> Unit)? = {}
override var onFullscreenChange: ((Boolean) -> Unit)? = {}
@@ -117,6 +129,11 @@ class VideoView @JvmOverloads constructor(
init {
addView(playerView)
setupFullscreenButton()
+ applyResizeMode()
+ }
+
+ private fun applyResizeMode() {
+ playerView.resizeMode = resizeMode.toAspectRatioFrameLayout()
}
private val layoutRunnable = Runnable {
@@ -328,6 +345,13 @@ class VideoView @JvmOverloads constructor(
removePipHelper()
removeFullscreenFragment()
VideoManager.unregisterView(this)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // We don't want activity to go to PiP Mode when video view is not presented
+ val currentActivity = applicationContent.currentActivity
+ currentActivity?.setPictureInPictureParams(createDisabledPictureInPictureParams(this))
+ }
+
super.onDetachedFromWindow()
}
diff --git a/packages/react-native-video/ios/core/Extensions/AVPlayerItem+externalSubtitles.swift b/packages/react-native-video/ios/core/Extensions/AVPlayerItem+externalSubtitles.swift
index 7d8e209d..ec47ced6 100644
--- a/packages/react-native-video/ios/core/Extensions/AVPlayerItem+externalSubtitles.swift
+++ b/packages/react-native-video/ios/core/Extensions/AVPlayerItem+externalSubtitles.swift
@@ -43,9 +43,11 @@ extension AVPlayerItem {
for textTrack in textTracks {
// Add subtitle track
if let compositionTextTrack = composition.addMutableTrack(withMediaType: .text, preferredTrackID: kCMPersistentTrackID_Invalid) {
- try compositionTextTrack.insertTimeRange(CMTimeRange(start: .zero, duration: textTrack.timeRange.duration), of: textTrack, at: .zero)
+ // We will trim the subtitle track to the duration of the video to match android behavior
+ try compositionTextTrack.insertTimeRange(CMTimeRange(start: .zero, duration: textTrack.timeRange.duration), of: videoTrack, at: .zero)
compositionTextTrack.languageCode = textTrack.languageCode
+ compositionTextTrack.isEnabled = false // Disable by default
}
}
diff --git a/packages/react-native-video/ios/core/Extensions/ResizeMode+VideoGravity.swift b/packages/react-native-video/ios/core/Extensions/ResizeMode+VideoGravity.swift
new file mode 100644
index 00000000..22a209ac
--- /dev/null
+++ b/packages/react-native-video/ios/core/Extensions/ResizeMode+VideoGravity.swift
@@ -0,0 +1,24 @@
+//
+// ResizeMode.swift
+// ReactNativeVideo
+//
+// Created for resizeMode feature
+//
+
+import Foundation
+import AVFoundation
+
+public extension ResizeMode {
+ func toVideoGravity() -> AVLayerVideoGravity {
+ switch self {
+ case .contain:
+ return .resizeAspect
+ case .cover:
+ return .resizeAspectFill
+ case .stretch:
+ return .resize
+ case .none:
+ return .resizeAspect // Default to aspect ratio if none specified
+ }
+ }
+}
diff --git a/packages/react-native-video/ios/core/VideoManager.swift b/packages/react-native-video/ios/core/VideoManager.swift
index eaf97b70..27b300c8 100644
--- a/packages/react-native-video/ios/core/VideoManager.swift
+++ b/packages/react-native-video/ios/core/VideoManager.swift
@@ -37,6 +37,34 @@ class VideoManager {
name: AVAudioSession.routeChangeNotification,
object: nil
)
+
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(applicationWillResignActive(notification:)),
+ name: UIApplication.willResignActiveNotification,
+ object: nil
+ )
+
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(applicationDidBecomeActive(notification:)),
+ name: UIApplication.didBecomeActiveNotification,
+ object: nil
+ )
+
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(applicationDidEnterBackground(notification:)),
+ name: UIApplication.didEnterBackgroundNotification,
+ object: nil
+ )
+
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(applicationWillEnterForeground(notification:)),
+ name: UIApplication.willEnterForegroundNotification,
+ object: nil
+ )
}
deinit {
@@ -99,7 +127,11 @@ class VideoManager {
hybridPlayer.player?.isMuted == false && hybridPlayer.player?.rate != 0
}
- if isAnyPlayerPlaying {
+ let anyPlayerNeedsNotMixWithOthers = players.allObjects.contains { player in
+ player.mixAudioMode == .donotmix
+ }
+
+ if isAnyPlayerPlaying || anyPlayerNeedsNotMixWithOthers {
activateAudioSession()
} else {
deactivateAudioSession()
@@ -110,26 +142,53 @@ class VideoManager {
private func configureAudioSession() {
let audioSession = AVAudioSession.sharedInstance()
+ var audioSessionCategoryOptions: AVAudioSession.CategoryOptions = audioSession.categoryOptions
let anyViewNeedsPictureInPicture = videoView.allObjects.contains { view in
view.allowsPictureInPicturePlayback
}
+ let anyPlayerNeedsSilentSwitchObey = players.allObjects.contains { player in
+ player.ignoreSilentSwitchMode == .obey
+ }
+
+ let anyPlayerNeedsSilentSwitchIgnore = players.allObjects.contains { player in
+ player.ignoreSilentSwitchMode == .ignore
+ }
+
+ let anyPlayerNeedsBackgroundPlayback = players.allObjects.contains { player in
+ player.playInBackground
+ }
+
if isAudioSessionManagementDisabled {
return
}
let category: AVAudioSession.Category = determineAudioCategory(
- silentSwitchObey: false, // TODO: Pass actual value after we add prop
- silentSwitchIgnore: false, // TODO: Pass actual value after we add prop
+ silentSwitchObey: anyPlayerNeedsSilentSwitchObey,
+ silentSwitchIgnore: anyPlayerNeedsSilentSwitchIgnore,
earpiece: false, // TODO: Pass actual value after we add prop
pip: anyViewNeedsPictureInPicture,
- backgroundPlayback: false, // TODO: Pass actual value after we add prop
+ backgroundPlayback: anyPlayerNeedsBackgroundPlayback,
notificationControls: false // TODO: Pass actual value after we add prop
)
+ let audioMixingMode = determineAudioMixingMode()
+
+ switch audioMixingMode {
+ case .mixwithothers:
+ audioSessionCategoryOptions.insert(.mixWithOthers)
+ case .donotmix:
+ audioSessionCategoryOptions.remove(.mixWithOthers)
+ case .duckothers:
+ audioSessionCategoryOptions.insert(.duckOthers)
+ case .auto:
+ audioSessionCategoryOptions.remove(.mixWithOthers)
+ audioSessionCategoryOptions.remove(.duckOthers)
+ }
+
do {
- try audioSession.setCategory(category)
+ try audioSession.setCategory(category, mode: .moviePlayback, options: audioSessionCategoryOptions)
} catch {
print("ReactNativeVideo: Failed to set audio session category: \(error.localizedDescription)")
}
@@ -187,6 +246,46 @@ class VideoManager {
return .playback
}
+ func determineAudioMixingMode() -> MixAudioMode {
+ let activePlayers = players.allObjects.filter { player in
+ player.isPlaying && player.player?.isMuted != true
+ }
+
+ if activePlayers.isEmpty {
+ return .mixwithothers
+ }
+
+ let anyPlayerNeedsMixWithOthers = activePlayers.contains { player in
+ player.mixAudioMode == .mixwithothers
+ }
+
+ let anyPlayerNeedsNotMixWithOthers = activePlayers.contains { player in
+ player.mixAudioMode == .donotmix
+ }
+
+ let anyPlayerNeedsDucksOthers = activePlayers.contains { player in
+ player.mixAudioMode == .duckothers
+ }
+
+ let anyPlayerHasAutoMixAudioMode = activePlayers.contains { player in
+ player.mixAudioMode == .auto
+ }
+
+ if anyPlayerNeedsNotMixWithOthers {
+ return .donotmix
+ }
+
+ if anyPlayerHasAutoMixAudioMode {
+ return .auto
+ }
+
+ if anyPlayerNeedsDucksOthers {
+ return .duckothers
+ }
+
+ return .mixwithothers
+ }
+
// MARK: - Notification Handlers
@@ -235,4 +334,48 @@ class VideoManager {
break
}
}
+
+ @objc func applicationWillResignActive(notification: Notification) {
+ // Pause all players when the app is about to become inactive
+ for player in players.allObjects {
+ if player.playInBackground || player.playWhenInactive || !player.isPlaying || player.player?.isExternalPlaybackActive == true {
+ continue
+ }
+
+ try? player.pause()
+ player.wasAutoPaused = true
+ }
+ }
+
+ @objc func applicationDidBecomeActive(notification: Notification) {
+ // Resume all players when the app becomes active
+ for player in players.allObjects {
+ if player.wasAutoPaused {
+ try? player.play()
+ player.wasAutoPaused = false
+ }
+ }
+ }
+
+ @objc func applicationDidEnterBackground(notification: Notification) {
+ // Pause all players when the app enters background
+ for player in players.allObjects {
+ if player.playInBackground || player.player?.isExternalPlaybackActive == true || !player.isPlaying {
+ continue
+ }
+
+ try? player.pause()
+ player.wasAutoPaused = true
+ }
+ }
+
+ @objc func applicationWillEnterForeground(notification: Notification) {
+ // Resume all players when the app enters foreground
+ for player in players.allObjects {
+ if player.wasAutoPaused {
+ try? player.play()
+ player.wasAutoPaused = false
+ }
+ }
+ }
}
diff --git a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift
index 1dd1b8db..343f9c2f 100644
--- a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift
+++ b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift
@@ -144,6 +144,27 @@ class HybridVideoPlayer: HybridVideoPlayerSpec {
}
var loop: Bool = false
+
+ var mixAudioMode: MixAudioMode = .auto {
+ didSet {
+ VideoManager.shared.requestAudioSessionUpdate()
+ }
+ }
+
+ var ignoreSilentSwitchMode: IgnoreSilentSwitchMode = .auto {
+ didSet {
+ VideoManager.shared.requestAudioSessionUpdate()
+ }
+ }
+
+ var playInBackground: Bool = false
+
+ var playWhenInactive: Bool = false
+
+ var wasAutoPaused: Bool = false
+
+ // Text track selection state
+ private var selectedExternalTrackIndex: Int? = nil
var isPlaying: Bool {
get {
@@ -248,8 +269,8 @@ class HybridVideoPlayer: HybridVideoPlayerSpec {
return
}
- self.playerItem = try await self.initializePlayerItem()
self.source = source
+ self.playerItem = try await self.initializePlayerItem()
playerQueue.sync {
do {
@@ -322,12 +343,132 @@ class HybridVideoPlayer: HybridVideoPlayerSpec {
throw SourceError.failedToInitializeAsset.error()
}
+ let playerItem: AVPlayerItem
+
// iOS does not support external subtitles for HLS streams
if let externalSubtiles = source.config.externalSubtitles, externalSubtiles.isEmpty == false, source.uri.hasSuffix(".m3u8") == false {
- return try await AVPlayerItem.withExternalSubtitles(for: asset, config: source.config)
+ playerItem = try await AVPlayerItem.withExternalSubtitles(for: asset, config: source.config)
+ } else {
+ playerItem = AVPlayerItem(asset: asset)
}
- return AVPlayerItem(asset: asset)
+ return playerItem
+ }
+
+ // MARK: - Text Track Management
+
+ func getAvailableTextTracks() throws -> [TextTrack] {
+ guard let currentItem = playerPointer.currentItem else {
+ return []
+ }
+
+ var tracks: [TextTrack] = []
+
+ // Get all text tracks from the media selection group (includes both built-in and external)
+ if let mediaSelection = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) {
+ for (index, option) in mediaSelection.options.enumerated() {
+ let isSelected = currentItem.currentMediaSelection.selectedMediaOption(in: mediaSelection) == option
+
+ // Determine if this is an external track based on the display name or other characteristics
+ let isExternal = source.config.externalSubtitles?.contains { subtitle in
+ option.displayName.contains(subtitle.label)
+ } ?? false
+
+ let trackId = isExternal ? "external-\(index)" : "builtin-\(option.displayName)-\(option.locale?.identifier ?? "unknown")"
+
+ tracks.append(TextTrack(
+ id: trackId,
+ label: option.displayName,
+ language: option.locale?.identifier,
+ selected: isSelected
+ ))
+ }
+ }
+
+ return tracks
+ }
+
+ func selectTextTrack(textTrack: TextTrack?) throws {
+ guard let currentItem = playerPointer.currentItem else {
+ throw PlayerError.notInitialized.error()
+ }
+
+ guard let mediaSelection = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else {
+ return
+ }
+
+ // If textTrack is nil, disable all text tracks
+ guard let textTrack = textTrack else {
+ currentItem.select(nil, in: mediaSelection)
+ selectedExternalTrackIndex = nil
+ eventEmitter.onTrackChange(nil)
+ return
+ }
+
+ if textTrack.id.isEmpty {
+ // Disable all text tracks
+ currentItem.select(nil, in: mediaSelection)
+ selectedExternalTrackIndex = nil
+ eventEmitter.onTrackChange(nil)
+ return
+ }
+
+ // Find and select the track by matching the ID
+ if textTrack.id.hasPrefix("external-") {
+ let trackIndexStr = String(textTrack.id.dropFirst("external-".count))
+ if let trackIndex = Int(trackIndexStr), trackIndex < mediaSelection.options.count {
+ let option = mediaSelection.options[trackIndex]
+ currentItem.select(option, in: mediaSelection)
+ selectedExternalTrackIndex = trackIndex
+ eventEmitter.onTrackChange(textTrack)
+ }
+ } else if textTrack.id.hasPrefix("builtin-") {
+ // Handle built-in tracks
+ for option in mediaSelection.options {
+ let optionId = "builtin-\(option.displayName)-\(option.locale?.identifier ?? "unknown")"
+ if optionId == textTrack.id {
+ currentItem.select(option, in: mediaSelection)
+ selectedExternalTrackIndex = nil
+ eventEmitter.onTrackChange(textTrack)
+ return
+ }
+ }
+ }
+ }
+
+ var selectedTrack: TextTrack? {
+ get {
+ guard let currentItem = playerPointer.currentItem else {
+ return nil
+ }
+
+ guard let mediaSelection = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else {
+ return nil
+ }
+
+ guard let selectedOption = currentItem.currentMediaSelection.selectedMediaOption(in: mediaSelection) else {
+ return nil
+ }
+
+ // Find the index of the selected option
+ guard let index = mediaSelection.options.firstIndex(of: selectedOption) else {
+ return nil
+ }
+
+ // Determine if this is an external track based on the display name or other characteristics
+ let isExternal = source.config.externalSubtitles?.contains { subtitle in
+ selectedOption.displayName.contains(subtitle.label)
+ } ?? false
+
+ let trackId = isExternal ? "external-\(index)" : "builtin-\(selectedOption.displayName)-\(selectedOption.locale?.identifier ?? "unknown")"
+
+ return TextTrack(
+ id: trackId,
+ label: selectedOption.displayName,
+ language: selectedOption.locale?.identifier,
+ selected: true
+ )
+ }
}
// MARK: - Memory Management
diff --git a/packages/react-native-video/ios/hybrids/VideoPlayerEmitter/HybridVideoPlayerEventEmitter.swift b/packages/react-native-video/ios/hybrids/VideoPlayerEmitter/HybridVideoPlayerEventEmitter.swift
index 065dbedd..e1516783 100644
--- a/packages/react-native-video/ios/hybrids/VideoPlayerEmitter/HybridVideoPlayerEventEmitter.swift
+++ b/packages/react-native-video/ios/hybrids/VideoPlayerEmitter/HybridVideoPlayerEventEmitter.swift
@@ -43,5 +43,7 @@ class HybridVideoPlayerEventEmitter: HybridVideoPlayerEventEmitterSpec {
var onTextTrackDataChanged: ([String]) -> Void = { _ in }
+ var onTrackChange: ((TextTrack?) -> Void) = { _ in }
+
var onVolumeChange: ((Double) -> Void) = { _ in }
}
diff --git a/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSource.swift b/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSource.swift
index 9eec8cb3..e0defde3 100644
--- a/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSource.swift
+++ b/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSource.swift
@@ -80,9 +80,9 @@ class HybridVideoPlayerSource: HybridVideoPlayerSourceSpec {
throw SourceError.failedToInitializeAsset.error()
}
- // Code browed from expo-video https://github.com/expo/expo/blob/ea17c9b1ce5111e1454b089ba381f3feb93f33cc/packages/expo-video/ios/VideoPlayerItem.swift#L40C30-L40C73
- // If we don't load those properties, they will be loaded on main thread casuing lags
- _ = try? await asset.load(.duration, .preferredTransform, .isPlayable)
+ // Code browned from expo-video https://github.com/expo/expo/blob/ea17c9b1ce5111e1454b089ba381f3feb93f33cc/packages/expo-video/ios/VideoPlayerItem.swift#L40C30-L40C73
+ // If we don't load those properties, they will be loaded on main thread causing lags
+ _ = try? await asset.load(.duration, .preferredTransform, .isPlayable) as Any
}
public func releaseAsset() {
diff --git a/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSourceFactory.swift b/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSourceFactory.swift
index 1d9ef726..eed77695 100644
--- a/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSourceFactory.swift
+++ b/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSourceFactory.swift
@@ -13,7 +13,7 @@ class HybridVideoPlayerSourceFactory: HybridVideoPlayerSourceFactorySpec {
}
func fromUri(uri: String) throws -> HybridVideoPlayerSourceSpec {
- let config = NativeVideoConfig(uri: uri, headers: nil, externalSubtitles: nil)
+ let config = NativeVideoConfig(uri: uri, externalSubtitles: nil, headers: nil)
return try HybridVideoPlayerSource(config: config)
}
}
diff --git a/packages/react-native-video/ios/hybrids/VideoViewViewManager/HybridVideoViewViewManager.swift b/packages/react-native-video/ios/hybrids/VideoViewViewManager/HybridVideoViewViewManager.swift
index 99287353..58de7ec2 100644
--- a/packages/react-native-video/ios/hybrids/VideoViewViewManager/HybridVideoViewViewManager.swift
+++ b/packages/react-native-video/ios/hybrids/VideoViewViewManager/HybridVideoViewViewManager.swift
@@ -100,6 +100,25 @@ class HybridVideoViewViewManager: HybridVideoViewViewManagerSpec {
}
}
+ var resizeMode: ResizeMode {
+ get {
+ guard let view else {
+ print(DEALOCATED_WARNING)
+ return .none
+ }
+
+ return view.resizeMode
+ }
+ set {
+ guard let view else {
+ print(DEALOCATED_WARNING)
+ return
+ }
+
+ view.resizeMode = newValue
+ }
+ }
+
func enterFullscreen() throws {
guard let view else {
throw VideoViewError.viewIsDeallocated.error()
diff --git a/packages/react-native-video/ios/view/VideoComponentView.swift b/packages/react-native-video/ios/view/VideoComponentView.swift
index 713cc4c9..6537cb13 100644
--- a/packages/react-native-video/ios/view/VideoComponentView.swift
+++ b/packages/react-native-video/ios/view/VideoComponentView.swift
@@ -67,6 +67,15 @@ import AVKit
}
}
+ public var resizeMode: ResizeMode = .none {
+ didSet {
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self, let playerViewController = self.playerViewController else { return }
+ playerViewController.videoGravity = resizeMode.toVideoGravity()
+ }
+ }
+ }
+
@objc public var nitroId: NSNumber = -1 {
didSet {
VideoComponentView.globalViewsMap.setObject(self, forKey: nitroId)
@@ -121,6 +130,7 @@ import AVKit
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = controls
+ controller.videoGravity = self.resizeMode.toVideoGravity()
controller.view.frame = playerView.bounds
controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
controller.view.backgroundColor = .clear
@@ -207,6 +217,7 @@ import AVKit
DispatchQueue.main.async {
// Here we skip error handling for simplicity
+ // We do check for PiP support earlier in the code
playerViewController.stopPictureInPicture()
}
}
diff --git a/packages/react-native-video/nitrogen/generated/android/ReactNativeVideoOnLoad.cpp b/packages/react-native-video/nitrogen/generated/android/ReactNativeVideoOnLoad.cpp
index cd0babe4..59a7fcbd 100644
--- a/packages/react-native-video/nitrogen/generated/android/ReactNativeVideoOnLoad.cpp
+++ b/packages/react-native-video/nitrogen/generated/android/ReactNativeVideoOnLoad.cpp
@@ -28,6 +28,7 @@
#include "JFunc_void_onProgressData.hpp"
#include "JFunc_void_TimedMetadata.hpp"
#include "JFunc_void_std__vector_std__string_.hpp"
+#include "JFunc_void_std__optional_TextTrack_.hpp"
#include "JFunc_void_VideoPlayerStatus.hpp"
#include "JHybridVideoPlayerSourceSpec.hpp"
#include "JHybridVideoPlayerSourceFactorySpec.hpp"
@@ -58,6 +59,7 @@ int initialize(JavaVM* vm) {
margelo::nitro::video::JFunc_void_onProgressData_cxx::registerNatives();
margelo::nitro::video::JFunc_void_TimedMetadata_cxx::registerNatives();
margelo::nitro::video::JFunc_void_std__vector_std__string__cxx::registerNatives();
+ margelo::nitro::video::JFunc_void_std__optional_TextTrack__cxx::registerNatives();
margelo::nitro::video::JFunc_void_VideoPlayerStatus_cxx::registerNatives();
margelo::nitro::video::JHybridVideoPlayerSourceSpec::registerNatives();
margelo::nitro::video::JHybridVideoPlayerSourceFactorySpec::registerNatives();
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JFunc_void_std__optional_TextTrack_.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JFunc_void_std__optional_TextTrack_.hpp
new file mode 100644
index 00000000..07d0e4fc
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JFunc_void_std__optional_TextTrack_.hpp
@@ -0,0 +1,78 @@
+///
+/// JFunc_void_std__optional_TextTrack_.hpp
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+#pragma once
+
+#include
+#include
+
+#include
+#include
+#include "TextTrack.hpp"
+#include "JTextTrack.hpp"
+#include
+
+namespace margelo::nitro::video {
+
+ using namespace facebook;
+
+ /**
+ * Represents the Java/Kotlin callback `(track: TextTrack?) -> Unit`.
+ * This can be passed around between C++ and Java/Kotlin.
+ */
+ struct JFunc_void_std__optional_TextTrack_: public jni::JavaClass {
+ public:
+ static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/video/Func_void_std__optional_TextTrack_;";
+
+ public:
+ /**
+ * Invokes the function this `JFunc_void_std__optional_TextTrack_` instance holds through JNI.
+ */
+ void invoke(const std::optional& track) const {
+ static const auto method = javaClassStatic()->getMethod /* track */)>("invoke");
+ method(self(), track.has_value() ? JTextTrack::fromCpp(track.value()) : nullptr);
+ }
+ };
+
+ /**
+ * An implementation of Func_void_std__optional_TextTrack_ that is backed by a C++ implementation (using `std::function<...>`)
+ */
+ struct JFunc_void_std__optional_TextTrack__cxx final: public jni::HybridClass {
+ public:
+ static jni::local_ref fromCpp(const std::function& /* track */)>& func) {
+ return JFunc_void_std__optional_TextTrack__cxx::newObjectCxxArgs(func);
+ }
+
+ public:
+ /**
+ * Invokes the C++ `std::function<...>` this `JFunc_void_std__optional_TextTrack__cxx` instance holds.
+ */
+ void invoke_cxx(jni::alias_ref track) {
+ _func(track != nullptr ? std::make_optional(track->toCpp()) : std::nullopt);
+ }
+
+ public:
+ [[nodiscard]]
+ inline const std::function& /* track */)>& getFunction() const {
+ return _func;
+ }
+
+ public:
+ static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/video/Func_void_std__optional_TextTrack__cxx;";
+ static void registerNatives() {
+ registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_std__optional_TextTrack__cxx::invoke_cxx)});
+ }
+
+ private:
+ explicit JFunc_void_std__optional_TextTrack__cxx(const std::function& /* track */)>& func): _func(func) { }
+
+ private:
+ friend HybridBase;
+ std::function& /* track */)> _func;
+ };
+
+} // namespace margelo::nitro::video
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerEventEmitterSpec.cpp b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerEventEmitterSpec.cpp
index d8fb13c7..7ba7e923 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerEventEmitterSpec.cpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerEventEmitterSpec.cpp
@@ -27,6 +27,8 @@ namespace margelo::nitro::video { struct onProgressData; }
namespace margelo::nitro::video { struct TimedMetadata; }
// Forward declaration of `TimedMetadataObject` to properly resolve imports.
namespace margelo::nitro::video { struct TimedMetadataObject; }
+// Forward declaration of `TextTrack` to properly resolve imports.
+namespace margelo::nitro::video { struct TextTrack; }
// Forward declaration of `VideoPlayerStatus` to properly resolve imports.
namespace margelo::nitro::video { enum class VideoPlayerStatus; }
@@ -66,6 +68,9 @@ namespace margelo::nitro::video { enum class VideoPlayerStatus; }
#include "JTimedMetadataObject.hpp"
#include
#include "JFunc_void_std__vector_std__string_.hpp"
+#include "TextTrack.hpp"
+#include "JFunc_void_std__optional_TextTrack_.hpp"
+#include "JTextTrack.hpp"
#include "VideoPlayerStatus.hpp"
#include "JFunc_void_VideoPlayerStatus.hpp"
#include "JVideoPlayerStatus.hpp"
@@ -376,6 +381,24 @@ namespace margelo::nitro::video {
static const auto method = javaClassStatic()->getMethod /* onTextTrackDataChanged */)>("setOnTextTrackDataChanged_cxx");
method(_javaPart, JFunc_void_std__vector_std__string__cxx::fromCpp(onTextTrackDataChanged));
}
+ std::function& /* track */)> JHybridVideoPlayerEventEmitterSpec::getOnTrackChange() {
+ static const auto method = javaClassStatic()->getMethod()>("getOnTrackChange_cxx");
+ auto __result = method(_javaPart);
+ return [&]() -> std::function& /* track */)> {
+ if (__result->isInstanceOf(JFunc_void_std__optional_TextTrack__cxx::javaClassStatic())) [[likely]] {
+ auto downcast = jni::static_ref_cast(__result);
+ return downcast->cthis()->getFunction();
+ } else {
+ return [__result](std::optional track) -> void {
+ return __result->invoke(track);
+ };
+ }
+ }();
+ }
+ void JHybridVideoPlayerEventEmitterSpec::setOnTrackChange(const std::function& /* track */)>& onTrackChange) {
+ static const auto method = javaClassStatic()->getMethod /* onTrackChange */)>("setOnTrackChange_cxx");
+ method(_javaPart, JFunc_void_std__optional_TextTrack__cxx::fromCpp(onTrackChange));
+ }
std::function JHybridVideoPlayerEventEmitterSpec::getOnVolumeChange() {
static const auto method = javaClassStatic()->getMethod()>("getOnVolumeChange_cxx");
auto __result = method(_javaPart);
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerEventEmitterSpec.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerEventEmitterSpec.hpp
index 61fb6aa9..a8debce6 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerEventEmitterSpec.hpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerEventEmitterSpec.hpp
@@ -79,6 +79,8 @@ namespace margelo::nitro::video {
void setOnTimedMetadata(const std::function& onTimedMetadata) override;
std::function& /* texts */)> getOnTextTrackDataChanged() override;
void setOnTextTrackDataChanged(const std::function& /* texts */)>& onTextTrackDataChanged) override;
+ std::function& /* track */)> getOnTrackChange() override;
+ void setOnTrackChange(const std::function& /* track */)>& onTrackChange) override;
std::function getOnVolumeChange() override;
void setOnVolumeChange(const std::function& onVolumeChange) override;
std::function getOnStatusChange() override;
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSourceFactorySpec.cpp b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSourceFactorySpec.cpp
index 171e338e..78a2c14c 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSourceFactorySpec.cpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSourceFactorySpec.cpp
@@ -11,8 +11,10 @@
namespace margelo::nitro::video { class HybridVideoPlayerSourceSpec; }
// Forward declaration of `NativeVideoConfig` to properly resolve imports.
namespace margelo::nitro::video { struct NativeVideoConfig; }
-// Forward declaration of `ExternalSubtitle` to properly resolve imports.
-namespace margelo::nitro::video { struct ExternalSubtitle; }
+// Forward declaration of `NativeExternalSubtitle` to properly resolve imports.
+namespace margelo::nitro::video { struct NativeExternalSubtitle; }
+// Forward declaration of `SubtitleType` to properly resolve imports.
+namespace margelo::nitro::video { enum class SubtitleType; }
#include
#include "HybridVideoPlayerSourceSpec.hpp"
@@ -22,10 +24,12 @@ namespace margelo::nitro::video { struct ExternalSubtitle; }
#include "NativeVideoConfig.hpp"
#include "JNativeVideoConfig.hpp"
#include
-#include
#include
-#include "ExternalSubtitle.hpp"
-#include "JExternalSubtitle.hpp"
+#include "NativeExternalSubtitle.hpp"
+#include "JNativeExternalSubtitle.hpp"
+#include "SubtitleType.hpp"
+#include "JSubtitleType.hpp"
+#include
namespace margelo::nitro::video {
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSourceSpec.cpp b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSourceSpec.cpp
index 36dee035..e0b88197 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSourceSpec.cpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSourceSpec.cpp
@@ -9,8 +9,10 @@
// Forward declaration of `NativeVideoConfig` to properly resolve imports.
namespace margelo::nitro::video { struct NativeVideoConfig; }
-// Forward declaration of `ExternalSubtitle` to properly resolve imports.
-namespace margelo::nitro::video { struct ExternalSubtitle; }
+// Forward declaration of `NativeExternalSubtitle` to properly resolve imports.
+namespace margelo::nitro::video { struct NativeExternalSubtitle; }
+// Forward declaration of `SubtitleType` to properly resolve imports.
+namespace margelo::nitro::video { enum class SubtitleType; }
// Forward declaration of `VideoInformation` to properly resolve imports.
namespace margelo::nitro::video { struct VideoInformation; }
// Forward declaration of `VideoOrientation` to properly resolve imports.
@@ -20,10 +22,12 @@ namespace margelo::nitro::video { enum class VideoOrientation; }
#include "NativeVideoConfig.hpp"
#include "JNativeVideoConfig.hpp"
#include
-#include
#include
-#include "ExternalSubtitle.hpp"
-#include "JExternalSubtitle.hpp"
+#include "NativeExternalSubtitle.hpp"
+#include "JNativeExternalSubtitle.hpp"
+#include "SubtitleType.hpp"
+#include "JSubtitleType.hpp"
+#include
#include
#include "VideoInformation.hpp"
#include
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.cpp b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.cpp
index fd8671ac..2ba0b40f 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.cpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.cpp
@@ -13,6 +13,12 @@ namespace margelo::nitro::video { class HybridVideoPlayerSourceSpec; }
namespace margelo::nitro::video { class HybridVideoPlayerEventEmitterSpec; }
// Forward declaration of `VideoPlayerStatus` to properly resolve imports.
namespace margelo::nitro::video { enum class VideoPlayerStatus; }
+// Forward declaration of `MixAudioMode` to properly resolve imports.
+namespace margelo::nitro::video { enum class MixAudioMode; }
+// Forward declaration of `IgnoreSilentSwitchMode` to properly resolve imports.
+namespace margelo::nitro::video { enum class IgnoreSilentSwitchMode; }
+// Forward declaration of `TextTrack` to properly resolve imports.
+namespace margelo::nitro::video { struct TextTrack; }
#include
#include "HybridVideoPlayerSourceSpec.hpp"
@@ -22,9 +28,17 @@ namespace margelo::nitro::video { enum class VideoPlayerStatus; }
#include "JHybridVideoPlayerEventEmitterSpec.hpp"
#include "VideoPlayerStatus.hpp"
#include "JVideoPlayerStatus.hpp"
+#include "MixAudioMode.hpp"
+#include "JMixAudioMode.hpp"
+#include "IgnoreSilentSwitchMode.hpp"
+#include "JIgnoreSilentSwitchMode.hpp"
+#include
+#include "TextTrack.hpp"
+#include "JTextTrack.hpp"
+#include
#include
#include
-#include
+#include
namespace margelo::nitro::video {
@@ -109,11 +123,52 @@ namespace margelo::nitro::video {
static const auto method = javaClassStatic()->getMethod("setRate");
method(_javaPart, rate);
}
+ MixAudioMode JHybridVideoPlayerSpec::getMixAudioMode() {
+ static const auto method = javaClassStatic()->getMethod()>("getMixAudioMode");
+ auto __result = method(_javaPart);
+ return __result->toCpp();
+ }
+ void JHybridVideoPlayerSpec::setMixAudioMode(MixAudioMode mixAudioMode) {
+ static const auto method = javaClassStatic()->getMethod /* mixAudioMode */)>("setMixAudioMode");
+ method(_javaPart, JMixAudioMode::fromCpp(mixAudioMode));
+ }
+ IgnoreSilentSwitchMode JHybridVideoPlayerSpec::getIgnoreSilentSwitchMode() {
+ static const auto method = javaClassStatic()->getMethod()>("getIgnoreSilentSwitchMode");
+ auto __result = method(_javaPart);
+ return __result->toCpp();
+ }
+ void JHybridVideoPlayerSpec::setIgnoreSilentSwitchMode(IgnoreSilentSwitchMode ignoreSilentSwitchMode) {
+ static const auto method = javaClassStatic()->getMethod /* ignoreSilentSwitchMode */)>("setIgnoreSilentSwitchMode");
+ method(_javaPart, JIgnoreSilentSwitchMode::fromCpp(ignoreSilentSwitchMode));
+ }
+ bool JHybridVideoPlayerSpec::getPlayInBackground() {
+ static const auto method = javaClassStatic()->getMethod("getPlayInBackground");
+ auto __result = method(_javaPart);
+ return static_cast(__result);
+ }
+ void JHybridVideoPlayerSpec::setPlayInBackground(bool playInBackground) {
+ static const auto method = javaClassStatic()->getMethod("setPlayInBackground");
+ method(_javaPart, playInBackground);
+ }
+ bool JHybridVideoPlayerSpec::getPlayWhenInactive() {
+ static const auto method = javaClassStatic()->getMethod("getPlayWhenInactive");
+ auto __result = method(_javaPart);
+ return static_cast(__result);
+ }
+ void JHybridVideoPlayerSpec::setPlayWhenInactive(bool playWhenInactive) {
+ static const auto method = javaClassStatic()->getMethod("setPlayWhenInactive");
+ method(_javaPart, playWhenInactive);
+ }
bool JHybridVideoPlayerSpec::getIsPlaying() {
static const auto method = javaClassStatic()->getMethod("isPlaying");
auto __result = method(_javaPart);
return static_cast(__result);
}
+ std::optional JHybridVideoPlayerSpec::getSelectedTrack() {
+ static const auto method = javaClassStatic()->getMethod()>("getSelectedTrack");
+ auto __result = method(_javaPart);
+ return __result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt;
+ }
// Methods
std::shared_ptr> JHybridVideoPlayerSpec::replaceSourceAsync(const std::optional>& source) {
@@ -131,6 +186,24 @@ namespace margelo::nitro::video {
return __promise;
}();
}
+ std::vector JHybridVideoPlayerSpec::getAvailableTextTracks() {
+ static const auto method = javaClassStatic()->getMethod>()>("getAvailableTextTracks");
+ auto __result = method(_javaPart);
+ return [&]() {
+ size_t __size = __result->size();
+ std::vector __vector;
+ __vector.reserve(__size);
+ for (size_t __i = 0; __i < __size; __i++) {
+ auto __element = __result->getElement(__i);
+ __vector.push_back(__element->toCpp());
+ }
+ return __vector;
+ }();
+ }
+ void JHybridVideoPlayerSpec::selectTextTrack(const std::optional& textTrack) {
+ static const auto method = javaClassStatic()->getMethod /* textTrack */)>("selectTextTrack");
+ method(_javaPart, textTrack.has_value() ? JTextTrack::fromCpp(textTrack.value()) : nullptr);
+ }
void JHybridVideoPlayerSpec::clean() {
static const auto method = javaClassStatic()->getMethod("clean");
method(_javaPart);
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.hpp
index cdf7d0fc..0c283e78 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.hpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.hpp
@@ -61,11 +61,22 @@ namespace margelo::nitro::video {
void setLoop(bool loop) override;
double getRate() override;
void setRate(double rate) override;
+ MixAudioMode getMixAudioMode() override;
+ void setMixAudioMode(MixAudioMode mixAudioMode) override;
+ IgnoreSilentSwitchMode getIgnoreSilentSwitchMode() override;
+ void setIgnoreSilentSwitchMode(IgnoreSilentSwitchMode ignoreSilentSwitchMode) override;
+ bool getPlayInBackground() override;
+ void setPlayInBackground(bool playInBackground) override;
+ bool getPlayWhenInactive() override;
+ void setPlayWhenInactive(bool playWhenInactive) override;
bool getIsPlaying() override;
+ std::optional getSelectedTrack() override;
public:
// Methods
std::shared_ptr> replaceSourceAsync(const std::optional>& source) override;
+ std::vector getAvailableTextTracks() override;
+ void selectTextTrack(const std::optional& textTrack) override;
void clean() override;
std::shared_ptr> preload() override;
void play() override;
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoViewViewManagerSpec.cpp b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoViewViewManagerSpec.cpp
index 5d71a9e0..51a58169 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoViewViewManagerSpec.cpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoViewViewManagerSpec.cpp
@@ -9,12 +9,16 @@
// Forward declaration of `HybridVideoPlayerSpec` to properly resolve imports.
namespace margelo::nitro::video { class HybridVideoPlayerSpec; }
+// Forward declaration of `ResizeMode` to properly resolve imports.
+namespace margelo::nitro::video { enum class ResizeMode; }
#include
#include
#include "HybridVideoPlayerSpec.hpp"
#include "JHybridVideoPlayerSpec.hpp"
#include
+#include "ResizeMode.hpp"
+#include "JResizeMode.hpp"
#include
#include "JFunc_void_bool.hpp"
#include "JFunc_void.hpp"
@@ -73,6 +77,15 @@ namespace margelo::nitro::video {
static const auto method = javaClassStatic()->getMethod("setAutoEnterPictureInPicture");
method(_javaPart, autoEnterPictureInPicture);
}
+ ResizeMode JHybridVideoViewViewManagerSpec::getResizeMode() {
+ static const auto method = javaClassStatic()->getMethod()>("getResizeMode");
+ auto __result = method(_javaPart);
+ return __result->toCpp();
+ }
+ void JHybridVideoViewViewManagerSpec::setResizeMode(ResizeMode resizeMode) {
+ static const auto method = javaClassStatic()->getMethod /* resizeMode */)>("setResizeMode");
+ method(_javaPart, JResizeMode::fromCpp(resizeMode));
+ }
std::optional> JHybridVideoViewViewManagerSpec::getOnPictureInPictureChange() {
static const auto method = javaClassStatic()->getMethod()>("getOnPictureInPictureChange_cxx");
auto __result = method(_javaPart);
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoViewViewManagerSpec.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoViewViewManagerSpec.hpp
index 70746841..358052b5 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoViewViewManagerSpec.hpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoViewViewManagerSpec.hpp
@@ -55,6 +55,8 @@ namespace margelo::nitro::video {
void setPictureInPicture(bool pictureInPicture) override;
bool getAutoEnterPictureInPicture() override;
void setAutoEnterPictureInPicture(bool autoEnterPictureInPicture) override;
+ ResizeMode getResizeMode() override;
+ void setResizeMode(ResizeMode resizeMode) override;
std::optional> getOnPictureInPictureChange() override;
void setOnPictureInPictureChange(const std::optional>& onPictureInPictureChange) override;
std::optional> getOnFullscreenChange() override;
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JIgnoreSilentSwitchMode.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JIgnoreSilentSwitchMode.hpp
new file mode 100644
index 00000000..fa76999e
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JIgnoreSilentSwitchMode.hpp
@@ -0,0 +1,62 @@
+///
+/// JIgnoreSilentSwitchMode.hpp
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+#pragma once
+
+#include
+#include "IgnoreSilentSwitchMode.hpp"
+
+namespace margelo::nitro::video {
+
+ using namespace facebook;
+
+ /**
+ * The C++ JNI bridge between the C++ enum "IgnoreSilentSwitchMode" and the the Kotlin enum "IgnoreSilentSwitchMode".
+ */
+ struct JIgnoreSilentSwitchMode final: public jni::JavaClass {
+ public:
+ static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/video/IgnoreSilentSwitchMode;";
+
+ public:
+ /**
+ * Convert this Java/Kotlin-based enum to the C++ enum IgnoreSilentSwitchMode.
+ */
+ [[maybe_unused]]
+ [[nodiscard]]
+ IgnoreSilentSwitchMode toCpp() const {
+ static const auto clazz = javaClassStatic();
+ static const auto fieldOrdinal = clazz->getField("_ordinal");
+ int ordinal = this->getFieldValue(fieldOrdinal);
+ return static_cast(ordinal);
+ }
+
+ public:
+ /**
+ * Create a Java/Kotlin-based enum with the given C++ enum's value.
+ */
+ [[maybe_unused]]
+ static jni::alias_ref fromCpp(IgnoreSilentSwitchMode value) {
+ static const auto clazz = javaClassStatic();
+ static const auto fieldAUTO = clazz->getStaticField("AUTO");
+ static const auto fieldIGNORE = clazz->getStaticField("IGNORE");
+ static const auto fieldOBEY = clazz->getStaticField("OBEY");
+
+ switch (value) {
+ case IgnoreSilentSwitchMode::AUTO:
+ return clazz->getStaticFieldValue(fieldAUTO);
+ case IgnoreSilentSwitchMode::IGNORE:
+ return clazz->getStaticFieldValue(fieldIGNORE);
+ case IgnoreSilentSwitchMode::OBEY:
+ return clazz->getStaticFieldValue(fieldOBEY);
+ default:
+ std::string stringValue = std::to_string(static_cast(value));
+ throw std::invalid_argument("Invalid enum value (" + stringValue + "!");
+ }
+ }
+ };
+
+} // namespace margelo::nitro::video
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JMixAudioMode.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JMixAudioMode.hpp
new file mode 100644
index 00000000..022c9036
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JMixAudioMode.hpp
@@ -0,0 +1,65 @@
+///
+/// JMixAudioMode.hpp
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+#pragma once
+
+#include
+#include "MixAudioMode.hpp"
+
+namespace margelo::nitro::video {
+
+ using namespace facebook;
+
+ /**
+ * The C++ JNI bridge between the C++ enum "MixAudioMode" and the the Kotlin enum "MixAudioMode".
+ */
+ struct JMixAudioMode final: public jni::JavaClass {
+ public:
+ static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/video/MixAudioMode;";
+
+ public:
+ /**
+ * Convert this Java/Kotlin-based enum to the C++ enum MixAudioMode.
+ */
+ [[maybe_unused]]
+ [[nodiscard]]
+ MixAudioMode toCpp() const {
+ static const auto clazz = javaClassStatic();
+ static const auto fieldOrdinal = clazz->getField("_ordinal");
+ int ordinal = this->getFieldValue(fieldOrdinal);
+ return static_cast(ordinal);
+ }
+
+ public:
+ /**
+ * Create a Java/Kotlin-based enum with the given C++ enum's value.
+ */
+ [[maybe_unused]]
+ static jni::alias_ref fromCpp(MixAudioMode value) {
+ static const auto clazz = javaClassStatic();
+ static const auto fieldMIXWITHOTHERS = clazz->getStaticField("MIXWITHOTHERS");
+ static const auto fieldDONOTMIX = clazz->getStaticField("DONOTMIX");
+ static const auto fieldDUCKOTHERS = clazz->getStaticField("DUCKOTHERS");
+ static const auto fieldAUTO = clazz->getStaticField("AUTO");
+
+ switch (value) {
+ case MixAudioMode::MIXWITHOTHERS:
+ return clazz->getStaticFieldValue(fieldMIXWITHOTHERS);
+ case MixAudioMode::DONOTMIX:
+ return clazz->getStaticFieldValue(fieldDONOTMIX);
+ case MixAudioMode::DUCKOTHERS:
+ return clazz->getStaticFieldValue(fieldDUCKOTHERS);
+ case MixAudioMode::AUTO:
+ return clazz->getStaticFieldValue(fieldAUTO);
+ default:
+ std::string stringValue = std::to_string(static_cast(value));
+ throw std::invalid_argument("Invalid enum value (" + stringValue + "!");
+ }
+ }
+ };
+
+} // namespace margelo::nitro::video
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JExternalSubtitle.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JNativeExternalSubtitle.hpp
similarity index 53%
rename from packages/react-native-video/nitrogen/generated/android/c++/JExternalSubtitle.hpp
rename to packages/react-native-video/nitrogen/generated/android/c++/JNativeExternalSubtitle.hpp
index 01aa662d..6da70c6e 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JExternalSubtitle.hpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JNativeExternalSubtitle.hpp
@@ -1,5 +1,5 @@
///
-/// JExternalSubtitle.hpp
+/// JNativeExternalSubtitle.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © 2025 Marc Rousavy @ Margelo
@@ -8,8 +8,10 @@
#pragma once
#include
-#include "ExternalSubtitle.hpp"
+#include "NativeExternalSubtitle.hpp"
+#include "JSubtitleType.hpp"
+#include "SubtitleType.hpp"
#include
namespace margelo::nitro::video {
@@ -17,27 +19,30 @@ namespace margelo::nitro::video {
using namespace facebook;
/**
- * The C++ JNI bridge between the C++ struct "ExternalSubtitle" and the the Kotlin data class "ExternalSubtitle".
+ * The C++ JNI bridge between the C++ struct "NativeExternalSubtitle" and the the Kotlin data class "NativeExternalSubtitle".
*/
- struct JExternalSubtitle final: public jni::JavaClass {
+ struct JNativeExternalSubtitle final: public jni::JavaClass {
public:
- static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/video/ExternalSubtitle;";
+ static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/video/NativeExternalSubtitle;";
public:
/**
- * Convert this Java/Kotlin-based struct to the C++ struct ExternalSubtitle by copying all values to C++.
+ * Convert this Java/Kotlin-based struct to the C++ struct NativeExternalSubtitle by copying all values to C++.
*/
[[maybe_unused]]
[[nodiscard]]
- ExternalSubtitle toCpp() const {
+ NativeExternalSubtitle toCpp() const {
static const auto clazz = javaClassStatic();
static const auto fieldUri = clazz->getField("uri");
jni::local_ref uri = this->getFieldValue(fieldUri);
static const auto fieldLabel = clazz->getField("label");
jni::local_ref label = this->getFieldValue(fieldLabel);
- return ExternalSubtitle(
+ static const auto fieldType = clazz->getField("type");
+ jni::local_ref type = this->getFieldValue(fieldType);
+ return NativeExternalSubtitle(
uri->toStdString(),
- label->toStdString()
+ label->toStdString(),
+ type->toCpp()
);
}
@@ -46,10 +51,11 @@ namespace margelo::nitro::video {
* Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java.
*/
[[maybe_unused]]
- static jni::local_ref fromCpp(const ExternalSubtitle& value) {
+ static jni::local_ref fromCpp(const NativeExternalSubtitle& value) {
return newInstance(
jni::make_jstring(value.uri),
- jni::make_jstring(value.label)
+ jni::make_jstring(value.label),
+ JSubtitleType::fromCpp(value.type)
);
}
};
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JNativeVideoConfig.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JNativeVideoConfig.hpp
index 1e96dab2..585544ac 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JNativeVideoConfig.hpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JNativeVideoConfig.hpp
@@ -10,8 +10,10 @@
#include
#include "NativeVideoConfig.hpp"
-#include "ExternalSubtitle.hpp"
-#include "JExternalSubtitle.hpp"
+#include "JNativeExternalSubtitle.hpp"
+#include "JSubtitleType.hpp"
+#include "NativeExternalSubtitle.hpp"
+#include "SubtitleType.hpp"
#include
#include
#include
@@ -38,12 +40,22 @@ namespace margelo::nitro::video {
static const auto clazz = javaClassStatic();
static const auto fieldUri = clazz->getField("uri");
jni::local_ref uri = this->getFieldValue(fieldUri);
+ static const auto fieldExternalSubtitles = clazz->getField>("externalSubtitles");
+ jni::local_ref> externalSubtitles = this->getFieldValue(fieldExternalSubtitles);
static const auto fieldHeaders = clazz->getField>("headers");
jni::local_ref> headers = this->getFieldValue(fieldHeaders);
- static const auto fieldExternalSubtitles = clazz->getField>("externalSubtitles");
- jni::local_ref> externalSubtitles = this->getFieldValue(fieldExternalSubtitles);
return NativeVideoConfig(
uri->toStdString(),
+ externalSubtitles != nullptr ? std::make_optional([&]() {
+ size_t __size = externalSubtitles->size();
+ std::vector __vector;
+ __vector.reserve(__size);
+ for (size_t __i = 0; __i < __size; __i++) {
+ auto __element = externalSubtitles->getElement(__i);
+ __vector.push_back(__element->toCpp());
+ }
+ return __vector;
+ }()) : std::nullopt,
headers != nullptr ? std::make_optional([&]() {
std::unordered_map __map;
__map.reserve(headers->size());
@@ -51,16 +63,6 @@ namespace margelo::nitro::video {
__map.emplace(__entry.first->toStdString(), __entry.second->toStdString());
}
return __map;
- }()) : std::nullopt,
- externalSubtitles != nullptr ? std::make_optional([&]() {
- size_t __size = externalSubtitles->size();
- std::vector __vector;
- __vector.reserve(__size);
- for (size_t __i = 0; __i < __size; __i++) {
- auto __element = externalSubtitles->getElement(__i);
- __vector.push_back(__element->toCpp());
- }
- return __vector;
}()) : std::nullopt
);
}
@@ -73,21 +75,21 @@ namespace margelo::nitro::video {
static jni::local_ref fromCpp(const NativeVideoConfig& value) {
return newInstance(
jni::make_jstring(value.uri),
+ value.externalSubtitles.has_value() ? [&]() {
+ size_t __size = value.externalSubtitles.value().size();
+ jni::local_ref> __array = jni::JArrayClass::newArray(__size);
+ for (size_t __i = 0; __i < __size; __i++) {
+ const auto& __element = value.externalSubtitles.value()[__i];
+ __array->setElement(__i, *JNativeExternalSubtitle::fromCpp(__element));
+ }
+ return __array;
+ }() : nullptr,
value.headers.has_value() ? [&]() -> jni::local_ref> {
auto __map = jni::JHashMap::create(value.headers.value().size());
for (const auto& __entry : value.headers.value()) {
__map->put(jni::make_jstring(__entry.first), jni::make_jstring(__entry.second));
}
return __map;
- }() : nullptr,
- value.externalSubtitles.has_value() ? [&]() {
- size_t __size = value.externalSubtitles.value().size();
- jni::local_ref> __array = jni::JArrayClass::newArray(__size);
- for (size_t __i = 0; __i < __size; __i++) {
- const auto& __element = value.externalSubtitles.value()[__i];
- __array->setElement(__i, *JExternalSubtitle::fromCpp(__element));
- }
- return __array;
}() : nullptr
);
}
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JResizeMode.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JResizeMode.hpp
new file mode 100644
index 00000000..6eb0857e
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JResizeMode.hpp
@@ -0,0 +1,65 @@
+///
+/// JResizeMode.hpp
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+#pragma once
+
+#include
+#include "ResizeMode.hpp"
+
+namespace margelo::nitro::video {
+
+ using namespace facebook;
+
+ /**
+ * The C++ JNI bridge between the C++ enum "ResizeMode" and the the Kotlin enum "ResizeMode".
+ */
+ struct JResizeMode final: public jni::JavaClass {
+ public:
+ static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/video/ResizeMode;";
+
+ public:
+ /**
+ * Convert this Java/Kotlin-based enum to the C++ enum ResizeMode.
+ */
+ [[maybe_unused]]
+ [[nodiscard]]
+ ResizeMode toCpp() const {
+ static const auto clazz = javaClassStatic();
+ static const auto fieldOrdinal = clazz->getField("_ordinal");
+ int ordinal = this->getFieldValue(fieldOrdinal);
+ return static_cast(ordinal);
+ }
+
+ public:
+ /**
+ * Create a Java/Kotlin-based enum with the given C++ enum's value.
+ */
+ [[maybe_unused]]
+ static jni::alias_ref fromCpp(ResizeMode value) {
+ static const auto clazz = javaClassStatic();
+ static const auto fieldCONTAIN = clazz->getStaticField("CONTAIN");
+ static const auto fieldCOVER = clazz->getStaticField("COVER");
+ static const auto fieldSTRETCH = clazz->getStaticField("STRETCH");
+ static const auto fieldNONE = clazz->getStaticField("NONE");
+
+ switch (value) {
+ case ResizeMode::CONTAIN:
+ return clazz->getStaticFieldValue(fieldCONTAIN);
+ case ResizeMode::COVER:
+ return clazz->getStaticFieldValue(fieldCOVER);
+ case ResizeMode::STRETCH:
+ return clazz->getStaticFieldValue(fieldSTRETCH);
+ case ResizeMode::NONE:
+ return clazz->getStaticFieldValue(fieldNONE);
+ default:
+ std::string stringValue = std::to_string(static_cast(value));
+ throw std::invalid_argument("Invalid enum value (" + stringValue + "!");
+ }
+ }
+ };
+
+} // namespace margelo::nitro::video
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JSubtitleType.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JSubtitleType.hpp
new file mode 100644
index 00000000..8f82f1c0
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JSubtitleType.hpp
@@ -0,0 +1,68 @@
+///
+/// JSubtitleType.hpp
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+#pragma once
+
+#include
+#include "SubtitleType.hpp"
+
+namespace margelo::nitro::video {
+
+ using namespace facebook;
+
+ /**
+ * The C++ JNI bridge between the C++ enum "SubtitleType" and the the Kotlin enum "SubtitleType".
+ */
+ struct JSubtitleType final: public jni::JavaClass {
+ public:
+ static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/video/SubtitleType;";
+
+ public:
+ /**
+ * Convert this Java/Kotlin-based enum to the C++ enum SubtitleType.
+ */
+ [[maybe_unused]]
+ [[nodiscard]]
+ SubtitleType toCpp() const {
+ static const auto clazz = javaClassStatic();
+ static const auto fieldOrdinal = clazz->getField("_ordinal");
+ int ordinal = this->getFieldValue(fieldOrdinal);
+ return static_cast(ordinal);
+ }
+
+ public:
+ /**
+ * Create a Java/Kotlin-based enum with the given C++ enum's value.
+ */
+ [[maybe_unused]]
+ static jni::alias_ref fromCpp(SubtitleType value) {
+ static const auto clazz = javaClassStatic();
+ static const auto fieldAUTO = clazz->getStaticField("AUTO");
+ static const auto fieldVTT = clazz->getStaticField("VTT");
+ static const auto fieldSRT = clazz->getStaticField("SRT");
+ static const auto fieldSSA = clazz->getStaticField("SSA");
+ static const auto fieldASS = clazz->getStaticField("ASS");
+
+ switch (value) {
+ case SubtitleType::AUTO:
+ return clazz->getStaticFieldValue(fieldAUTO);
+ case SubtitleType::VTT:
+ return clazz->getStaticFieldValue(fieldVTT);
+ case SubtitleType::SRT:
+ return clazz->getStaticFieldValue(fieldSRT);
+ case SubtitleType::SSA:
+ return clazz->getStaticFieldValue(fieldSSA);
+ case SubtitleType::ASS:
+ return clazz->getStaticFieldValue(fieldASS);
+ default:
+ std::string stringValue = std::to_string(static_cast(value));
+ throw std::invalid_argument("Invalid enum value (" + stringValue + "!");
+ }
+ }
+ };
+
+} // namespace margelo::nitro::video
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JTextTrack.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JTextTrack.hpp
new file mode 100644
index 00000000..a6e6e177
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JTextTrack.hpp
@@ -0,0 +1,66 @@
+///
+/// JTextTrack.hpp
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+#pragma once
+
+#include
+#include "TextTrack.hpp"
+
+#include
+#include
+
+namespace margelo::nitro::video {
+
+ using namespace facebook;
+
+ /**
+ * The C++ JNI bridge between the C++ struct "TextTrack" and the the Kotlin data class "TextTrack".
+ */
+ struct JTextTrack final: public jni::JavaClass {
+ public:
+ static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/video/TextTrack;";
+
+ public:
+ /**
+ * Convert this Java/Kotlin-based struct to the C++ struct TextTrack by copying all values to C++.
+ */
+ [[maybe_unused]]
+ [[nodiscard]]
+ TextTrack toCpp() const {
+ static const auto clazz = javaClassStatic();
+ static const auto fieldId = clazz->getField("id");
+ jni::local_ref id = this->getFieldValue(fieldId);
+ static const auto fieldLabel = clazz->getField("label");
+ jni::local_ref label = this->getFieldValue(fieldLabel);
+ static const auto fieldLanguage = clazz->getField("language");
+ jni::local_ref language = this->getFieldValue(fieldLanguage);
+ static const auto fieldSelected = clazz->getField("selected");
+ jboolean selected = this->getFieldValue(fieldSelected);
+ return TextTrack(
+ id->toStdString(),
+ label->toStdString(),
+ language != nullptr ? std::make_optional(language->toStdString()) : std::nullopt,
+ static_cast(selected)
+ );
+ }
+
+ public:
+ /**
+ * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java.
+ */
+ [[maybe_unused]]
+ static jni::local_ref fromCpp(const TextTrack& value) {
+ return newInstance(
+ jni::make_jstring(value.id),
+ jni::make_jstring(value.label),
+ value.language.has_value() ? jni::make_jstring(value.language.value()) : nullptr,
+ value.selected
+ );
+ }
+ };
+
+} // namespace margelo::nitro::video
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/Func_void_std__optional_TextTrack_.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/Func_void_std__optional_TextTrack_.kt
new file mode 100644
index 00000000..58886739
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/Func_void_std__optional_TextTrack_.kt
@@ -0,0 +1,80 @@
+///
+/// Func_void_std__optional_TextTrack_.kt
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+package com.margelo.nitro.video
+
+import androidx.annotation.Keep
+import com.facebook.jni.HybridData
+import com.facebook.proguard.annotations.DoNotStrip
+import com.margelo.nitro.core.*
+import dalvik.annotation.optimization.FastNative
+
+/**
+ * Represents the JavaScript callback `(track: optional) => void`.
+ * This can be either implemented in C++ (in which case it might be a callback coming from JS),
+ * or in Kotlin/Java (in which case it is a native callback).
+ */
+@DoNotStrip
+@Keep
+@Suppress("ClassName", "RedundantUnitReturnType")
+fun interface Func_void_std__optional_TextTrack_: (TextTrack?) -> Unit {
+ /**
+ * Call the given JS callback.
+ * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted.
+ */
+ @DoNotStrip
+ @Keep
+ override fun invoke(track: TextTrack?): Unit
+}
+
+/**
+ * Represents the JavaScript callback `(track: optional) => void`.
+ * This is implemented in C++, via a `std::function<...>`.
+ * The callback might be coming from JS.
+ */
+@DoNotStrip
+@Keep
+@Suppress(
+ "KotlinJniMissingFunction", "unused",
+ "RedundantSuppression", "RedundantUnitReturnType", "FunctionName",
+ "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName",
+)
+class Func_void_std__optional_TextTrack__cxx: Func_void_std__optional_TextTrack_ {
+ @DoNotStrip
+ @Keep
+ private val mHybridData: HybridData
+
+ @DoNotStrip
+ @Keep
+ private constructor(hybridData: HybridData) {
+ mHybridData = hybridData
+ }
+
+ @DoNotStrip
+ @Keep
+ override fun invoke(track: TextTrack?): Unit
+ = invoke_cxx(track)
+
+ @FastNative
+ private external fun invoke_cxx(track: TextTrack?): Unit
+}
+
+/**
+ * Represents the JavaScript callback `(track: optional) => void`.
+ * This is implemented in Java/Kotlin, via a `(TextTrack?) -> Unit`.
+ * The callback is always coming from native.
+ */
+@DoNotStrip
+@Keep
+@Suppress("ClassName", "RedundantUnitReturnType", "unused")
+class Func_void_std__optional_TextTrack__java(private val function: (TextTrack?) -> Unit): Func_void_std__optional_TextTrack_ {
+ @DoNotStrip
+ @Keep
+ override fun invoke(track: TextTrack?): Unit {
+ return this.function(track)
+ }
+}
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerEventEmitterSpec.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerEventEmitterSpec.kt
index fe1bfdc2..f21dfa01 100644
--- a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerEventEmitterSpec.kt
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerEventEmitterSpec.kt
@@ -261,6 +261,20 @@ abstract class HybridVideoPlayerEventEmitterSpec: HybridObject() {
onTextTrackDataChanged = value
}
+ abstract var onTrackChange: (track: TextTrack?) -> Unit
+
+ private var onTrackChange_cxx: Func_void_std__optional_TextTrack_
+ @Keep
+ @DoNotStrip
+ get() {
+ return Func_void_std__optional_TextTrack__java(onTrackChange)
+ }
+ @Keep
+ @DoNotStrip
+ set(value) {
+ onTrackChange = value
+ }
+
abstract var onVolumeChange: (volume: Double) -> Unit
private var onVolumeChange_cxx: Func_void_double
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerSpec.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerSpec.kt
index e370c039..fe342ee2 100644
--- a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerSpec.kt
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerSpec.kt
@@ -83,15 +83,51 @@ abstract class HybridVideoPlayerSpec: HybridObject() {
@set:Keep
abstract var rate: Double
+ @get:DoNotStrip
+ @get:Keep
+ @set:DoNotStrip
+ @set:Keep
+ abstract var mixAudioMode: MixAudioMode
+
+ @get:DoNotStrip
+ @get:Keep
+ @set:DoNotStrip
+ @set:Keep
+ abstract var ignoreSilentSwitchMode: IgnoreSilentSwitchMode
+
+ @get:DoNotStrip
+ @get:Keep
+ @set:DoNotStrip
+ @set:Keep
+ abstract var playInBackground: Boolean
+
+ @get:DoNotStrip
+ @get:Keep
+ @set:DoNotStrip
+ @set:Keep
+ abstract var playWhenInactive: Boolean
+
@get:DoNotStrip
@get:Keep
abstract val isPlaying: Boolean
+
+ @get:DoNotStrip
+ @get:Keep
+ abstract val selectedTrack: TextTrack?
// Methods
@DoNotStrip
@Keep
abstract fun replaceSourceAsync(source: HybridVideoPlayerSourceSpec?): Promise
+ @DoNotStrip
+ @Keep
+ abstract fun getAvailableTextTracks(): Array
+
+ @DoNotStrip
+ @Keep
+ abstract fun selectTextTrack(textTrack: TextTrack?): Unit
+
@DoNotStrip
@Keep
abstract fun clean(): Unit
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoViewViewManagerSpec.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoViewViewManagerSpec.kt
index 9a4a5dea..59948fd6 100644
--- a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoViewViewManagerSpec.kt
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoViewViewManagerSpec.kt
@@ -61,6 +61,12 @@ abstract class HybridVideoViewViewManagerSpec: HybridObject() {
@set:Keep
abstract var autoEnterPictureInPicture: Boolean
+ @get:DoNotStrip
+ @get:Keep
+ @set:DoNotStrip
+ @set:Keep
+ abstract var resizeMode: ResizeMode
+
abstract var onPictureInPictureChange: ((isInPictureInPicture: Boolean) -> Unit)?
private var onPictureInPictureChange_cxx: Func_void_bool?
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/IgnoreSilentSwitchMode.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/IgnoreSilentSwitchMode.kt
new file mode 100644
index 00000000..36f7fcf8
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/IgnoreSilentSwitchMode.kt
@@ -0,0 +1,26 @@
+///
+/// IgnoreSilentSwitchMode.kt
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+package com.margelo.nitro.video
+
+import androidx.annotation.Keep
+import com.facebook.proguard.annotations.DoNotStrip
+
+/**
+ * Represents the JavaScript enum/union "IgnoreSilentSwitchMode".
+ */
+@DoNotStrip
+@Keep
+enum class IgnoreSilentSwitchMode {
+ AUTO,
+ IGNORE,
+ OBEY;
+
+ @DoNotStrip
+ @Keep
+ private val _ordinal = ordinal
+}
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/MixAudioMode.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/MixAudioMode.kt
new file mode 100644
index 00000000..7717f767
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/MixAudioMode.kt
@@ -0,0 +1,27 @@
+///
+/// MixAudioMode.kt
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+package com.margelo.nitro.video
+
+import androidx.annotation.Keep
+import com.facebook.proguard.annotations.DoNotStrip
+
+/**
+ * Represents the JavaScript enum/union "MixAudioMode".
+ */
+@DoNotStrip
+@Keep
+enum class MixAudioMode {
+ MIXWITHOTHERS,
+ DONOTMIX,
+ DUCKOTHERS,
+ AUTO;
+
+ @DoNotStrip
+ @Keep
+ private val _ordinal = ordinal
+}
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/ExternalSubtitle.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/NativeExternalSubtitle.kt
similarity index 69%
rename from packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/ExternalSubtitle.kt
rename to packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/NativeExternalSubtitle.kt
index 9c171c09..f226bee2 100644
--- a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/ExternalSubtitle.kt
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/NativeExternalSubtitle.kt
@@ -1,5 +1,5 @@
///
-/// ExternalSubtitle.kt
+/// NativeExternalSubtitle.kt
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © 2025 Marc Rousavy @ Margelo
@@ -12,16 +12,17 @@ import com.facebook.proguard.annotations.DoNotStrip
import com.margelo.nitro.core.*
/**
- * Represents the JavaScript object/struct "ExternalSubtitle".
+ * Represents the JavaScript object/struct "NativeExternalSubtitle".
*/
@DoNotStrip
@Keep
-data class ExternalSubtitle
+data class NativeExternalSubtitle
@DoNotStrip
@Keep
constructor(
val uri: String,
- val label: String
+ val label: String,
+ val type: SubtitleType
) {
/* main constructor */
}
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/NativeVideoConfig.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/NativeVideoConfig.kt
index 39f2f0c7..22ac431c 100644
--- a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/NativeVideoConfig.kt
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/NativeVideoConfig.kt
@@ -21,8 +21,8 @@ data class NativeVideoConfig
@Keep
constructor(
val uri: String,
- val headers: Map?,
- val externalSubtitles: Array?
+ val externalSubtitles: Array?,
+ val headers: Map?
) {
/* main constructor */
}
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/ResizeMode.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/ResizeMode.kt
new file mode 100644
index 00000000..95d43740
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/ResizeMode.kt
@@ -0,0 +1,27 @@
+///
+/// ResizeMode.kt
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+package com.margelo.nitro.video
+
+import androidx.annotation.Keep
+import com.facebook.proguard.annotations.DoNotStrip
+
+/**
+ * Represents the JavaScript enum/union "ResizeMode".
+ */
+@DoNotStrip
+@Keep
+enum class ResizeMode {
+ CONTAIN,
+ COVER,
+ STRETCH,
+ NONE;
+
+ @DoNotStrip
+ @Keep
+ private val _ordinal = ordinal
+}
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/SubtitleType.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/SubtitleType.kt
new file mode 100644
index 00000000..b7dc625a
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/SubtitleType.kt
@@ -0,0 +1,28 @@
+///
+/// SubtitleType.kt
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+package com.margelo.nitro.video
+
+import androidx.annotation.Keep
+import com.facebook.proguard.annotations.DoNotStrip
+
+/**
+ * Represents the JavaScript enum/union "SubtitleType".
+ */
+@DoNotStrip
+@Keep
+enum class SubtitleType {
+ AUTO,
+ VTT,
+ SRT,
+ SSA,
+ ASS;
+
+ @DoNotStrip
+ @Keep
+ private val _ordinal = ordinal
+}
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/TextTrack.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/TextTrack.kt
new file mode 100644
index 00000000..12cb3902
--- /dev/null
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/TextTrack.kt
@@ -0,0 +1,29 @@
+///
+/// TextTrack.kt
+/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
+/// https://github.com/mrousavy/nitro
+/// Copyright © 2025 Marc Rousavy @ Margelo
+///
+
+package com.margelo.nitro.video
+
+import androidx.annotation.Keep
+import com.facebook.proguard.annotations.DoNotStrip
+import com.margelo.nitro.core.*
+
+/**
+ * Represents the JavaScript object/struct "TextTrack".
+ */
+@DoNotStrip
+@Keep
+data class TextTrack
+ @DoNotStrip
+ @Keep
+ constructor(
+ val id: String,
+ val label: String,
+ val language: String?,
+ val selected: Boolean
+ ) {
+ /* main constructor */
+}
diff --git a/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Bridge.cpp b/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Bridge.cpp
index 998858eb..c0680c7c 100644
--- a/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Bridge.cpp
+++ b/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Bridge.cpp
@@ -171,6 +171,14 @@ namespace margelo::nitro::video::bridge::swift {
};
}
+ // pragma MARK: std::function& /* track */)>
+ Func_void_std__optional_TextTrack_ create_Func_void_std__optional_TextTrack_(void* _Nonnull swiftClosureWrapper) {
+ auto swiftClosure = ReactNativeVideo::Func_void_std__optional_TextTrack_::fromUnsafe(swiftClosureWrapper);
+ return [swiftClosure = std::move(swiftClosure)](const std::optional& track) mutable -> void {
+ swiftClosure.call(track);
+ };
+ }
+
// pragma MARK: std::function
Func_void_VideoPlayerStatus create_Func_void_VideoPlayerStatus(void* _Nonnull swiftClosureWrapper) {
auto swiftClosure = ReactNativeVideo::Func_void_VideoPlayerStatus::fromUnsafe(swiftClosureWrapper);
diff --git a/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Bridge.hpp b/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Bridge.hpp
index 59335a5b..bef8f82c 100644
--- a/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Bridge.hpp
+++ b/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Bridge.hpp
@@ -10,8 +10,6 @@
// Forward declarations of C++ defined types
// Forward declaration of `BandwidthData` to properly resolve imports.
namespace margelo::nitro::video { struct BandwidthData; }
-// Forward declaration of `ExternalSubtitle` to properly resolve imports.
-namespace margelo::nitro::video { struct ExternalSubtitle; }
// Forward declaration of `HybridVideoPlayerEventEmitterSpec` to properly resolve imports.
namespace margelo::nitro::video { class HybridVideoPlayerEventEmitterSpec; }
// Forward declaration of `HybridVideoPlayerFactorySpec` to properly resolve imports.
@@ -26,8 +24,14 @@ namespace margelo::nitro::video { class HybridVideoPlayerSpec; }
namespace margelo::nitro::video { class HybridVideoViewViewManagerFactorySpec; }
// Forward declaration of `HybridVideoViewViewManagerSpec` to properly resolve imports.
namespace margelo::nitro::video { class HybridVideoViewViewManagerSpec; }
+// Forward declaration of `NativeExternalSubtitle` to properly resolve imports.
+namespace margelo::nitro::video { struct NativeExternalSubtitle; }
// Forward declaration of `SourceType` to properly resolve imports.
namespace margelo::nitro::video { enum class SourceType; }
+// Forward declaration of `SubtitleType` to properly resolve imports.
+namespace margelo::nitro::video { enum class SubtitleType; }
+// Forward declaration of `TextTrack` to properly resolve imports.
+namespace margelo::nitro::video { struct TextTrack; }
// Forward declaration of `TimedMetadataObject` to properly resolve imports.
namespace margelo::nitro::video { struct TimedMetadataObject; }
// Forward declaration of `TimedMetadata` to properly resolve imports.
@@ -65,7 +69,6 @@ namespace ReactNativeVideo { class HybridVideoViewViewManagerSpec_cxx; }
// Include C++ defined types
#include "BandwidthData.hpp"
-#include "ExternalSubtitle.hpp"
#include "HybridVideoPlayerEventEmitterSpec.hpp"
#include "HybridVideoPlayerFactorySpec.hpp"
#include "HybridVideoPlayerSourceFactorySpec.hpp"
@@ -73,7 +76,10 @@ namespace ReactNativeVideo { class HybridVideoViewViewManagerSpec_cxx; }
#include "HybridVideoPlayerSpec.hpp"
#include "HybridVideoViewViewManagerFactorySpec.hpp"
#include "HybridVideoViewViewManagerSpec.hpp"
+#include "NativeExternalSubtitle.hpp"
#include "SourceType.hpp"
+#include "SubtitleType.hpp"
+#include "TextTrack.hpp"
#include "TimedMetadata.hpp"
#include "TimedMetadataObject.hpp"
#include "VideoInformation.hpp"
@@ -189,6 +195,35 @@ namespace margelo::nitro::video::bridge::swift {
return std::optional>(value);
}
+ // pragma MARK: std::optional
+ /**
+ * Specialized version of `std::optional`.
+ */
+ using std__optional_std__string_ = std::optional;
+ inline std::optional create_std__optional_std__string_(const std::string& value) {
+ return std::optional(value);
+ }
+
+ // pragma MARK: std::vector
+ /**
+ * Specialized version of `std::vector`.
+ */
+ using std__vector_TextTrack_ = std::vector;
+ inline std::vector create_std__vector_TextTrack_(size_t size) {
+ std::vector vector;
+ vector.reserve(size);
+ return vector;
+ }
+
+ // pragma MARK: std::optional
+ /**
+ * Specialized version of `std::optional`.
+ */
+ using std__optional_TextTrack_ = std::optional;
+ inline std::optional create_std__optional_TextTrack_(const TextTrack& value) {
+ return std::optional(value);
+ }
+
// pragma MARK: std::shared_ptr
/**
* Specialized version of `std::shared_ptr`.
@@ -210,6 +245,15 @@ namespace margelo::nitro::video::bridge::swift {
return Result>>::withError(error);
}
+ // pragma MARK: Result>
+ using Result_std__vector_TextTrack__ = Result>;
+ inline Result_std__vector_TextTrack__ create_Result_std__vector_TextTrack__(const std::vector& value) {
+ return Result>::withValue(value);
+ }
+ inline Result_std__vector_TextTrack__ create_Result_std__vector_TextTrack__(const std::exception_ptr& error) {
+ return Result>::withError(error);
+ }
+
// pragma MARK: Result
using Result_void_ = Result;
inline Result_void_ create_Result_void_() {
@@ -469,6 +513,28 @@ namespace margelo::nitro::video::bridge::swift {
return Func_void_std__vector_std__string__Wrapper(std::move(value));
}
+ // pragma MARK: std::function& /* track */)>
+ /**
+ * Specialized version of `std::function&)>`.
+ */
+ using Func_void_std__optional_TextTrack_ = std::function& /* track */)>;
+ /**
+ * Wrapper class for a `std::function& / * track * /)>`, this can be used from Swift.
+ */
+ class Func_void_std__optional_TextTrack__Wrapper final {
+ public:
+ explicit Func_void_std__optional_TextTrack__Wrapper(std::function& /* track */)>&& func): _function(std::make_shared& /* track */)>>(std::move(func))) {}
+ inline void call(std::optional track) const {
+ _function->operator()(track);
+ }
+ private:
+ std::shared_ptr