From 235bc3bea35b84dc2820e62d06c2c4219c5eb5b7 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Sun, 15 Jun 2025 20:07:04 +0200 Subject: [PATCH] feat: implement background audio playback and video enhancements (#15) --- .../android/app/src/main/AndroidManifest.xml | 13 +- example/ios/Podfile.lock | 4 +- example/src/App.tsx | 1097 ++++++++++++----- .../react-native-video/android/build.gradle | 3 + .../java/com/video/core/AudioFocusManager.kt | 233 ++++ .../main/java/com/video/core/VideoManager.kt | 47 +- .../ResizeMode+AspectRatioFrameLayout.kt | 20 + .../core/extensions/SubtitleType+toString.kt | 14 + .../VideoPlaybackService+ServiceManagment.kt | 48 + .../com/video/core/player/MediaItemUtils.kt | 10 +- .../playback/VideoPlaybackCallback.kt | 58 + .../services/playback/VideoPlaybackService.kt | 327 +++++ .../VideoPlaybackServiceConnection.kt | 56 + .../video/core/utils/PictureInPictureUtils.kt | 15 +- .../com/video/core/utils/TextTrackUtils.kt | 164 +++ .../java/com/video/core/utils/Threading.kt | 33 + .../hybrids/videoplayer/HybridVideoPlayer.kt | 134 +- .../HybridVideoPlayerEventEmitter.kt | 2 + .../HybridVideoViewViewManager.kt | 8 +- .../src/main/java/com/video/view/VideoView.kt | 26 +- .../AVPlayerItem+externalSubtitles.swift | 4 +- .../Extensions/ResizeMode+VideoGravity.swift | 24 + .../ios/core/VideoManager.swift | 153 ++- .../VideoPlayer/HybridVideoPlayer.swift | 147 ++- .../HybridVideoPlayerEventEmitter.swift | 2 + .../HybridVideoPlayerSource.swift | 6 +- .../HybridVideoPlayerSourceFactory.swift | 2 +- .../HybridVideoViewViewManager.swift | 19 + .../ios/view/VideoComponentView.swift | 11 + .../android/ReactNativeVideoOnLoad.cpp | 2 + .../JFunc_void_std__optional_TextTrack_.hpp | 78 ++ .../JHybridVideoPlayerEventEmitterSpec.cpp | 23 + .../JHybridVideoPlayerEventEmitterSpec.hpp | 2 + .../JHybridVideoPlayerSourceFactorySpec.cpp | 14 +- .../c++/JHybridVideoPlayerSourceSpec.cpp | 14 +- .../android/c++/JHybridVideoPlayerSpec.cpp | 75 +- .../android/c++/JHybridVideoPlayerSpec.hpp | 11 + .../c++/JHybridVideoViewViewManagerSpec.cpp | 13 + .../c++/JHybridVideoViewViewManagerSpec.hpp | 2 + .../android/c++/JIgnoreSilentSwitchMode.hpp | 62 + .../generated/android/c++/JMixAudioMode.hpp | 65 + ...btitle.hpp => JNativeExternalSubtitle.hpp} | 28 +- .../android/c++/JNativeVideoConfig.hpp | 48 +- .../generated/android/c++/JResizeMode.hpp | 65 + .../generated/android/c++/JSubtitleType.hpp | 68 + .../generated/android/c++/JTextTrack.hpp | 66 + .../Func_void_std__optional_TextTrack_.kt | 80 ++ .../HybridVideoPlayerEventEmitterSpec.kt | 14 + .../nitro/video/HybridVideoPlayerSpec.kt | 36 + .../video/HybridVideoViewViewManagerSpec.kt | 6 + .../nitro/video/IgnoreSilentSwitchMode.kt | 26 + .../com/margelo/nitro/video/MixAudioMode.kt | 27 + ...lSubtitle.kt => NativeExternalSubtitle.kt} | 9 +- .../margelo/nitro/video/NativeVideoConfig.kt | 4 +- .../com/margelo/nitro/video/ResizeMode.kt | 27 + .../com/margelo/nitro/video/SubtitleType.kt | 28 + .../com/margelo/nitro/video/TextTrack.kt | 29 + .../ios/ReactNativeVideo-Swift-Cxx-Bridge.cpp | 8 + .../ios/ReactNativeVideo-Swift-Cxx-Bridge.hpp | 112 +- .../ReactNativeVideo-Swift-Cxx-Umbrella.hpp | 21 +- ...HybridVideoPlayerEventEmitterSpecSwift.hpp | 10 + ...ybridVideoPlayerSourceFactorySpecSwift.hpp | 11 +- .../c++/HybridVideoPlayerSourceSpecSwift.hpp | 11 +- .../ios/c++/HybridVideoPlayerSpecSwift.hpp | 57 +- .../HybridVideoViewViewManagerSpecSwift.hpp | 10 + .../Func_void_std__optional_TextTrack_.swift | 52 + .../HybridVideoPlayerEventEmitterSpec.swift | 1 + ...ybridVideoPlayerEventEmitterSpec_cxx.swift | 25 + .../ios/swift/HybridVideoPlayerSpec.swift | 7 + .../ios/swift/HybridVideoPlayerSpec_cxx.swift | 92 ++ .../HybridVideoViewViewManagerSpec.swift | 1 + .../HybridVideoViewViewManagerSpec_cxx.swift | 11 + .../ios/swift/IgnoreSilentSwitchMode.swift | 44 + .../generated/ios/swift/MixAudioMode.swift | 48 + ...tle.swift => NativeExternalSubtitle.swift} | 25 +- .../ios/swift/NativeVideoConfig.swift | 86 +- .../generated/ios/swift/ResizeMode.swift | 48 + .../generated/ios/swift/SubtitleType.swift | 52 + .../generated/ios/swift/TextTrack.swift | 86 ++ .../c++/HybridVideoPlayerEventEmitterSpec.cpp | 2 + .../c++/HybridVideoPlayerEventEmitterSpec.hpp | 6 + .../shared/c++/HybridVideoPlayerSpec.cpp | 11 + .../shared/c++/HybridVideoPlayerSpec.hpp | 23 +- .../c++/HybridVideoViewViewManagerSpec.cpp | 2 + .../c++/HybridVideoViewViewManagerSpec.hpp | 5 + .../shared/c++/IgnoreSilentSwitchMode.hpp | 82 ++ .../generated/shared/c++/MixAudioMode.hpp | 86 ++ ...ubtitle.hpp => NativeExternalSubtitle.hpp} | 30 +- .../shared/c++/NativeVideoConfig.hpp | 20 +- .../generated/shared/c++/ResizeMode.hpp | 86 ++ .../generated/shared/c++/SubtitleType.hpp | 90 ++ .../generated/shared/c++/TextTrack.hpp | 82 ++ packages/react-native-video/package.json | 2 +- .../src/core/VideoPlayer.ts | 69 ++ .../src/core/VideoPlayerEvents.ts | 9 + .../src/core/hooks/useManagedInstance.ts | 17 +- .../src/core/hooks/useVideoPlayer.ts | 6 +- .../src/core/types/Events.ts | 6 + .../src/core/types/IgnoreSilentSwitchMode.ts | 1 + .../src/core/types/MixAudioMode.ts | 1 + .../src/core/types/ResizeMode.ts | 9 + .../src/core/types/TextTrack.ts | 22 + .../src/core/types/VideoConfig.ts | 47 +- .../src/core/types/VideoError.ts | 1 + .../src/core/types/VideoPlayerBase.ts | 64 + .../src/core/utils/sourceFactory.ts | 43 +- .../src/core/video-view/VideoView.tsx | 58 +- packages/react-native-video/src/index.tsx | 4 + .../src/spec/nitro/VideoPlayer.nitro.ts | 13 + .../spec/nitro/VideoViewViewManager.nitro.ts | 2 + 110 files changed, 4766 insertions(+), 553 deletions(-) create mode 100644 packages/react-native-video/android/src/main/java/com/video/core/AudioFocusManager.kt create mode 100644 packages/react-native-video/android/src/main/java/com/video/core/extensions/ResizeMode+AspectRatioFrameLayout.kt create mode 100644 packages/react-native-video/android/src/main/java/com/video/core/extensions/SubtitleType+toString.kt create mode 100644 packages/react-native-video/android/src/main/java/com/video/core/extensions/VideoPlaybackService+ServiceManagment.kt create mode 100644 packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackCallback.kt create mode 100644 packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackService.kt create mode 100644 packages/react-native-video/android/src/main/java/com/video/core/services/playback/VideoPlaybackServiceConnection.kt create mode 100644 packages/react-native-video/android/src/main/java/com/video/core/utils/TextTrackUtils.kt create mode 100644 packages/react-native-video/ios/core/Extensions/ResizeMode+VideoGravity.swift create mode 100644 packages/react-native-video/nitrogen/generated/android/c++/JFunc_void_std__optional_TextTrack_.hpp create mode 100644 packages/react-native-video/nitrogen/generated/android/c++/JIgnoreSilentSwitchMode.hpp create mode 100644 packages/react-native-video/nitrogen/generated/android/c++/JMixAudioMode.hpp rename packages/react-native-video/nitrogen/generated/android/c++/{JExternalSubtitle.hpp => JNativeExternalSubtitle.hpp} (53%) create mode 100644 packages/react-native-video/nitrogen/generated/android/c++/JResizeMode.hpp create mode 100644 packages/react-native-video/nitrogen/generated/android/c++/JSubtitleType.hpp create mode 100644 packages/react-native-video/nitrogen/generated/android/c++/JTextTrack.hpp create mode 100644 packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/Func_void_std__optional_TextTrack_.kt create mode 100644 packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/IgnoreSilentSwitchMode.kt create mode 100644 packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/MixAudioMode.kt rename packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/{ExternalSubtitle.kt => NativeExternalSubtitle.kt} (69%) create mode 100644 packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/ResizeMode.kt create mode 100644 packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/SubtitleType.kt create mode 100644 packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/TextTrack.kt create mode 100644 packages/react-native-video/nitrogen/generated/ios/swift/Func_void_std__optional_TextTrack_.swift create mode 100644 packages/react-native-video/nitrogen/generated/ios/swift/IgnoreSilentSwitchMode.swift create mode 100644 packages/react-native-video/nitrogen/generated/ios/swift/MixAudioMode.swift rename packages/react-native-video/nitrogen/generated/ios/swift/{ExternalSubtitle.swift => NativeExternalSubtitle.swift} (52%) create mode 100644 packages/react-native-video/nitrogen/generated/ios/swift/ResizeMode.swift create mode 100644 packages/react-native-video/nitrogen/generated/ios/swift/SubtitleType.swift create mode 100644 packages/react-native-video/nitrogen/generated/ios/swift/TextTrack.swift create mode 100644 packages/react-native-video/nitrogen/generated/shared/c++/IgnoreSilentSwitchMode.hpp create mode 100644 packages/react-native-video/nitrogen/generated/shared/c++/MixAudioMode.hpp rename packages/react-native-video/nitrogen/generated/shared/c++/{ExternalSubtitle.hpp => NativeExternalSubtitle.hpp} (62%) create mode 100644 packages/react-native-video/nitrogen/generated/shared/c++/ResizeMode.hpp create mode 100644 packages/react-native-video/nitrogen/generated/shared/c++/SubtitleType.hpp create mode 100644 packages/react-native-video/nitrogen/generated/shared/c++/TextTrack.hpp create mode 100644 packages/react-native-video/src/core/types/IgnoreSilentSwitchMode.ts create mode 100644 packages/react-native-video/src/core/types/MixAudioMode.ts create mode 100644 packages/react-native-video/src/core/types/ResizeMode.ts create mode 100644 packages/react-native-video/src/core/types/TextTrack.ts 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& /* track */)>> _function; + }; + Func_void_std__optional_TextTrack_ create_Func_void_std__optional_TextTrack_(void* _Nonnull swiftClosureWrapper); + inline Func_void_std__optional_TextTrack__Wrapper wrap_Func_void_std__optional_TextTrack_(Func_void_std__optional_TextTrack_ value) { + return Func_void_std__optional_TextTrack__Wrapper(std::move(value)); + } + // pragma MARK: std::function /** * Specialized version of `std::function`. @@ -491,6 +557,26 @@ namespace margelo::nitro::video::bridge::swift { return Func_void_VideoPlayerStatus_Wrapper(std::move(value)); } + // pragma MARK: std::vector + /** + * Specialized version of `std::vector`. + */ + using std__vector_NativeExternalSubtitle_ = std::vector; + inline std::vector create_std__vector_NativeExternalSubtitle_(size_t size) { + std::vector vector; + vector.reserve(size); + return vector; + } + + // pragma MARK: std::optional> + /** + * Specialized version of `std::optional>`. + */ + using std__optional_std__vector_NativeExternalSubtitle__ = std::optional>; + inline std::optional> create_std__optional_std__vector_NativeExternalSubtitle__(const std::vector& value) { + return std::optional>(value); + } + // pragma MARK: std::unordered_map /** * Specialized version of `std::unordered_map`. @@ -522,26 +608,6 @@ namespace margelo::nitro::video::bridge::swift { return std::optional>(value); } - // pragma MARK: std::vector - /** - * Specialized version of `std::vector`. - */ - using std__vector_ExternalSubtitle_ = std::vector; - inline std::vector create_std__vector_ExternalSubtitle_(size_t size) { - std::vector vector; - vector.reserve(size); - return vector; - } - - // pragma MARK: std::optional> - /** - * Specialized version of `std::optional>`. - */ - using std__optional_std__vector_ExternalSubtitle__ = std::optional>; - inline std::optional> create_std__optional_std__vector_ExternalSubtitle__(const std::vector& value) { - return std::optional>(value); - } - // pragma MARK: std::shared_ptr> /** * Specialized version of `std::shared_ptr>`. diff --git a/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Umbrella.hpp b/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Umbrella.hpp index e988b1f5..f32e445f 100644 --- a/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Umbrella.hpp +++ b/packages/react-native-video/nitrogen/generated/ios/ReactNativeVideo-Swift-Cxx-Umbrella.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,10 +24,22 @@ 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 `IgnoreSilentSwitchMode` to properly resolve imports. +namespace margelo::nitro::video { enum class IgnoreSilentSwitchMode; } +// Forward declaration of `MixAudioMode` to properly resolve imports. +namespace margelo::nitro::video { enum class MixAudioMode; } +// Forward declaration of `NativeExternalSubtitle` to properly resolve imports. +namespace margelo::nitro::video { struct NativeExternalSubtitle; } // Forward declaration of `NativeVideoConfig` to properly resolve imports. namespace margelo::nitro::video { struct NativeVideoConfig; } +// Forward declaration of `ResizeMode` to properly resolve imports. +namespace margelo::nitro::video { enum class ResizeMode; } // 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. @@ -51,7 +61,6 @@ namespace margelo::nitro::video { struct onProgressData; } // Include C++ defined types #include "BandwidthData.hpp" -#include "ExternalSubtitle.hpp" #include "HybridVideoPlayerEventEmitterSpec.hpp" #include "HybridVideoPlayerFactorySpec.hpp" #include "HybridVideoPlayerSourceFactorySpec.hpp" @@ -59,8 +68,14 @@ namespace margelo::nitro::video { struct onProgressData; } #include "HybridVideoPlayerSpec.hpp" #include "HybridVideoViewViewManagerFactorySpec.hpp" #include "HybridVideoViewViewManagerSpec.hpp" +#include "IgnoreSilentSwitchMode.hpp" +#include "MixAudioMode.hpp" +#include "NativeExternalSubtitle.hpp" #include "NativeVideoConfig.hpp" +#include "ResizeMode.hpp" #include "SourceType.hpp" +#include "SubtitleType.hpp" +#include "TextTrack.hpp" #include "TimedMetadata.hpp" #include "TimedMetadataObject.hpp" #include "VideoInformation.hpp" diff --git a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerEventEmitterSpecSwift.hpp b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerEventEmitterSpecSwift.hpp index 10da575a..be901458 100644 --- a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerEventEmitterSpecSwift.hpp +++ b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerEventEmitterSpecSwift.hpp @@ -32,6 +32,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; } @@ -50,6 +52,7 @@ namespace margelo::nitro::video { enum class VideoPlayerStatus; } #include #include "TimedMetadataObject.hpp" #include +#include "TextTrack.hpp" #include "VideoPlayerStatus.hpp" #include "ReactNativeVideo-Swift-Cxx-Umbrella.hpp" @@ -199,6 +202,13 @@ namespace margelo::nitro::video { inline void setOnTextTrackDataChanged(const std::function& /* texts */)>& onTextTrackDataChanged) noexcept override { _swiftPart.setOnTextTrackDataChanged(onTextTrackDataChanged); } + inline std::function& /* track */)> getOnTrackChange() noexcept override { + auto __result = _swiftPart.getOnTrackChange(); + return __result; + } + inline void setOnTrackChange(const std::function& /* track */)>& onTrackChange) noexcept override { + _swiftPart.setOnTrackChange(onTrackChange); + } inline std::function getOnVolumeChange() noexcept override { auto __result = _swiftPart.getOnVolumeChange(); return __result; diff --git a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSourceFactorySpecSwift.hpp b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSourceFactorySpecSwift.hpp index 3e1faad8..09a468d9 100644 --- a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSourceFactorySpecSwift.hpp +++ b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSourceFactorySpecSwift.hpp @@ -16,17 +16,20 @@ namespace ReactNativeVideo { class HybridVideoPlayerSourceFactorySpec_cxx; } 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" #include #include "NativeVideoConfig.hpp" #include -#include #include -#include "ExternalSubtitle.hpp" +#include "NativeExternalSubtitle.hpp" +#include "SubtitleType.hpp" +#include #include "ReactNativeVideo-Swift-Cxx-Umbrella.hpp" diff --git a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSourceSpecSwift.hpp b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSourceSpecSwift.hpp index d6b2014b..85720b3b 100644 --- a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSourceSpecSwift.hpp +++ b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSourceSpecSwift.hpp @@ -14,8 +14,10 @@ namespace ReactNativeVideo { class HybridVideoPlayerSourceSpec_cxx; } // 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. @@ -24,9 +26,10 @@ namespace margelo::nitro::video { enum class VideoOrientation; } #include #include "NativeVideoConfig.hpp" #include -#include #include -#include "ExternalSubtitle.hpp" +#include "NativeExternalSubtitle.hpp" +#include "SubtitleType.hpp" +#include #include #include "VideoInformation.hpp" #include "VideoOrientation.hpp" diff --git a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSpecSwift.hpp b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSpecSwift.hpp index 38ae768c..143404ce 100644 --- a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSpecSwift.hpp +++ b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSpecSwift.hpp @@ -18,13 +18,24 @@ 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" #include "HybridVideoPlayerEventEmitterSpec.hpp" #include "VideoPlayerStatus.hpp" -#include +#include "MixAudioMode.hpp" +#include "IgnoreSilentSwitchMode.hpp" #include +#include "TextTrack.hpp" +#include +#include +#include #include "ReactNativeVideo-Swift-Cxx-Umbrella.hpp" @@ -106,9 +117,39 @@ namespace margelo::nitro::video { inline void setRate(double rate) noexcept override { _swiftPart.setRate(std::forward(rate)); } + inline MixAudioMode getMixAudioMode() noexcept override { + auto __result = _swiftPart.getMixAudioMode(); + return static_cast(__result); + } + inline void setMixAudioMode(MixAudioMode mixAudioMode) noexcept override { + _swiftPart.setMixAudioMode(static_cast(mixAudioMode)); + } + inline IgnoreSilentSwitchMode getIgnoreSilentSwitchMode() noexcept override { + auto __result = _swiftPart.getIgnoreSilentSwitchMode(); + return static_cast(__result); + } + inline void setIgnoreSilentSwitchMode(IgnoreSilentSwitchMode ignoreSilentSwitchMode) noexcept override { + _swiftPart.setIgnoreSilentSwitchMode(static_cast(ignoreSilentSwitchMode)); + } + inline bool getPlayInBackground() noexcept override { + return _swiftPart.getPlayInBackground(); + } + inline void setPlayInBackground(bool playInBackground) noexcept override { + _swiftPart.setPlayInBackground(std::forward(playInBackground)); + } + inline bool getPlayWhenInactive() noexcept override { + return _swiftPart.getPlayWhenInactive(); + } + inline void setPlayWhenInactive(bool playWhenInactive) noexcept override { + _swiftPart.setPlayWhenInactive(std::forward(playWhenInactive)); + } inline bool getIsPlaying() noexcept override { return _swiftPart.isPlaying(); } + inline std::optional getSelectedTrack() noexcept override { + auto __result = _swiftPart.getSelectedTrack(); + return __result; + } public: // Methods @@ -120,6 +161,20 @@ namespace margelo::nitro::video { auto __value = std::move(__result.value()); return __value; } + inline std::vector getAvailableTextTracks() override { + auto __result = _swiftPart.getAvailableTextTracks(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline void selectTextTrack(const std::optional& textTrack) override { + auto __result = _swiftPart.selectTextTrack(textTrack); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } inline void clean() override { auto __result = _swiftPart.clean(); if (__result.hasError()) [[unlikely]] { diff --git a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoViewViewManagerSpecSwift.hpp b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoViewViewManagerSpecSwift.hpp index aca623b7..45417e87 100644 --- a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoViewViewManagerSpecSwift.hpp +++ b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoViewViewManagerSpecSwift.hpp @@ -14,10 +14,13 @@ namespace ReactNativeVideo { class HybridVideoViewViewManagerSpec_cxx; } // 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 "ResizeMode.hpp" #include #include "ReactNativeVideo-Swift-Cxx-Umbrella.hpp" @@ -80,6 +83,13 @@ namespace margelo::nitro::video { inline void setAutoEnterPictureInPicture(bool autoEnterPictureInPicture) noexcept override { _swiftPart.setAutoEnterPictureInPicture(std::forward(autoEnterPictureInPicture)); } + inline ResizeMode getResizeMode() noexcept override { + auto __result = _swiftPart.getResizeMode(); + return static_cast(__result); + } + inline void setResizeMode(ResizeMode resizeMode) noexcept override { + _swiftPart.setResizeMode(static_cast(resizeMode)); + } inline std::optional> getOnPictureInPictureChange() noexcept override { auto __result = _swiftPart.getOnPictureInPictureChange(); return __result; diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/Func_void_std__optional_TextTrack_.swift b/packages/react-native-video/nitrogen/generated/ios/swift/Func_void_std__optional_TextTrack_.swift new file mode 100644 index 00000000..db52f38e --- /dev/null +++ b/packages/react-native-video/nitrogen/generated/ios/swift/Func_void_std__optional_TextTrack_.swift @@ -0,0 +1,52 @@ +/// +/// Func_void_std__optional_TextTrack_.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ track: TextTrack?) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_std__optional_TextTrack_ { + public typealias bridge = margelo.nitro.video.bridge.swift + + private let closure: (_ track: TextTrack?) -> Void + + public init(_ closure: @escaping (_ track: TextTrack?) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(track: bridge.std__optional_TextTrack_) -> Void { + self.closure({ () -> TextTrack? in + if let __unwrapped = track.value { + return __unwrapped + } else { + return nil + } + }()) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_std__optional_TextTrack_`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__optional_TextTrack_ { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerEventEmitterSpec.swift b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerEventEmitterSpec.swift index 18572c5e..458c1f95 100644 --- a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerEventEmitterSpec.swift +++ b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerEventEmitterSpec.swift @@ -27,6 +27,7 @@ public protocol HybridVideoPlayerEventEmitterSpec_protocol: HybridObject { var onSeek: (_ seekTime: Double) -> Void { get set } var onTimedMetadata: (_ metadata: TimedMetadata) -> Void { get set } var onTextTrackDataChanged: (_ texts: [String]) -> Void { get set } + var onTrackChange: (_ track: TextTrack?) -> Void { get set } var onVolumeChange: (_ volume: Double) -> Void { get set } var onStatusChange: (_ status: VideoPlayerStatus) -> Void { get set } diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerEventEmitterSpec_cxx.swift b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerEventEmitterSpec_cxx.swift index 0f5fd5af..dfb2c817 100644 --- a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerEventEmitterSpec_cxx.swift +++ b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerEventEmitterSpec_cxx.swift @@ -407,6 +407,31 @@ public class HybridVideoPlayerEventEmitterSpec_cxx { } } + public final var onTrackChange: bridge.Func_void_std__optional_TextTrack_ { + @inline(__always) + get { + return { () -> bridge.Func_void_std__optional_TextTrack_ in + let __closureWrapper = Func_void_std__optional_TextTrack_(self.__implementation.onTrackChange) + return bridge.create_Func_void_std__optional_TextTrack_(__closureWrapper.toUnsafe()) + }() + } + @inline(__always) + set { + self.__implementation.onTrackChange = { () -> (TextTrack?) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_std__optional_TextTrack_(newValue) + return { (__track: TextTrack?) -> Void in + __wrappedFunction.call({ () -> bridge.std__optional_TextTrack_ in + if let __unwrappedValue = __track { + return bridge.create_std__optional_TextTrack_(__unwrappedValue) + } else { + return .init() + } + }()) + } + }() + } + } + public final var onVolumeChange: bridge.Func_void_double { @inline(__always) get { diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec.swift b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec.swift index a864bb9e..5857c87e 100644 --- a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec.swift +++ b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec.swift @@ -20,10 +20,17 @@ public protocol HybridVideoPlayerSpec_protocol: HybridObject { var muted: Bool { get set } var loop: Bool { get set } var rate: Double { get set } + var mixAudioMode: MixAudioMode { get set } + var ignoreSilentSwitchMode: IgnoreSilentSwitchMode { get set } + var playInBackground: Bool { get set } + var playWhenInactive: Bool { get set } var isPlaying: Bool { get } + var selectedTrack: TextTrack? { get } // Methods func replaceSourceAsync(source: (any HybridVideoPlayerSourceSpec)?) throws -> Promise + func getAvailableTextTracks() throws -> [TextTrack] + func selectTextTrack(textTrack: TextTrack?) throws -> Void func clean() throws -> Void func preload() throws -> Promise func play() throws -> Void diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec_cxx.swift b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec_cxx.swift index b1c839ef..daae5c98 100644 --- a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec_cxx.swift +++ b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec_cxx.swift @@ -186,12 +186,69 @@ public class HybridVideoPlayerSpec_cxx { } } + public final var mixAudioMode: Int32 { + @inline(__always) + get { + return self.__implementation.mixAudioMode.rawValue + } + @inline(__always) + set { + self.__implementation.mixAudioMode = margelo.nitro.video.MixAudioMode(rawValue: newValue)! + } + } + + public final var ignoreSilentSwitchMode: Int32 { + @inline(__always) + get { + return self.__implementation.ignoreSilentSwitchMode.rawValue + } + @inline(__always) + set { + self.__implementation.ignoreSilentSwitchMode = margelo.nitro.video.IgnoreSilentSwitchMode(rawValue: newValue)! + } + } + + public final var playInBackground: Bool { + @inline(__always) + get { + return self.__implementation.playInBackground + } + @inline(__always) + set { + self.__implementation.playInBackground = newValue + } + } + + public final var playWhenInactive: Bool { + @inline(__always) + get { + return self.__implementation.playWhenInactive + } + @inline(__always) + set { + self.__implementation.playWhenInactive = newValue + } + } + public final var isPlaying: Bool { @inline(__always) get { return self.__implementation.isPlaying } } + + public final var selectedTrack: bridge.std__optional_TextTrack_ { + @inline(__always) + get { + return { () -> bridge.std__optional_TextTrack_ in + if let __unwrappedValue = self.__implementation.selectedTrack { + return bridge.create_std__optional_TextTrack_(__unwrappedValue) + } else { + return .init() + } + }() + } + } // Methods @inline(__always) @@ -223,6 +280,41 @@ public class HybridVideoPlayerSpec_cxx { } } + @inline(__always) + public final func getAvailableTextTracks() -> bridge.Result_std__vector_TextTrack__ { + do { + let __result = try self.__implementation.getAvailableTextTracks() + let __resultCpp = { () -> bridge.std__vector_TextTrack_ in + var __vector = bridge.create_std__vector_TextTrack_(__result.count) + for __item in __result { + __vector.push_back(__item) + } + return __vector + }() + return bridge.create_Result_std__vector_TextTrack__(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__vector_TextTrack__(__exceptionPtr) + } + } + + @inline(__always) + public final func selectTextTrack(textTrack: bridge.std__optional_TextTrack_) -> bridge.Result_void_ { + do { + try self.__implementation.selectTextTrack(textTrack: { () -> TextTrack? in + if let __unwrapped = textTrack.value { + return __unwrapped + } else { + return nil + } + }()) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } + @inline(__always) public final func clean() -> bridge.Result_void_ { do { diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoViewViewManagerSpec.swift b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoViewViewManagerSpec.swift index ecd5cfc3..9919843a 100644 --- a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoViewViewManagerSpec.swift +++ b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoViewViewManagerSpec.swift @@ -15,6 +15,7 @@ public protocol HybridVideoViewViewManagerSpec_protocol: HybridObject { var controls: Bool { get set } var pictureInPicture: Bool { get set } var autoEnterPictureInPicture: Bool { get set } + var resizeMode: ResizeMode { get set } var onPictureInPictureChange: ((_ isInPictureInPicture: Bool) -> Void)? { get set } var onFullscreenChange: ((_ fullscreen: Bool) -> Void)? { get set } var willEnterFullscreen: (() -> Void)? { get set } diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoViewViewManagerSpec_cxx.swift b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoViewViewManagerSpec_cxx.swift index 18c18f95..3b65052d 100644 --- a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoViewViewManagerSpec_cxx.swift +++ b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoViewViewManagerSpec_cxx.swift @@ -160,6 +160,17 @@ public class HybridVideoViewViewManagerSpec_cxx { } } + public final var resizeMode: Int32 { + @inline(__always) + get { + return self.__implementation.resizeMode.rawValue + } + @inline(__always) + set { + self.__implementation.resizeMode = margelo.nitro.video.ResizeMode(rawValue: newValue)! + } + } + public final var onPictureInPictureChange: bridge.std__optional_std__function_void_bool____isInPictureInPicture______ { @inline(__always) get { diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/IgnoreSilentSwitchMode.swift b/packages/react-native-video/nitrogen/generated/ios/swift/IgnoreSilentSwitchMode.swift new file mode 100644 index 00000000..ed8d01e3 --- /dev/null +++ b/packages/react-native-video/nitrogen/generated/ios/swift/IgnoreSilentSwitchMode.swift @@ -0,0 +1,44 @@ +/// +/// IgnoreSilentSwitchMode.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `IgnoreSilentSwitchMode`, backed by a C++ enum. + */ +public typealias IgnoreSilentSwitchMode = margelo.nitro.video.IgnoreSilentSwitchMode + +public extension IgnoreSilentSwitchMode { + /** + * Get a IgnoreSilentSwitchMode for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "auto": + self = .auto + case "ignore": + self = .ignore + case "obey": + self = .obey + default: + return nil + } + } + + /** + * Get the String value this IgnoreSilentSwitchMode represents. + */ + var stringValue: String { + switch self { + case .auto: + return "auto" + case .ignore: + return "ignore" + case .obey: + return "obey" + } + } +} diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/MixAudioMode.swift b/packages/react-native-video/nitrogen/generated/ios/swift/MixAudioMode.swift new file mode 100644 index 00000000..425330b9 --- /dev/null +++ b/packages/react-native-video/nitrogen/generated/ios/swift/MixAudioMode.swift @@ -0,0 +1,48 @@ +/// +/// MixAudioMode.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `MixAudioMode`, backed by a C++ enum. + */ +public typealias MixAudioMode = margelo.nitro.video.MixAudioMode + +public extension MixAudioMode { + /** + * Get a MixAudioMode for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "mixWithOthers": + self = .mixwithothers + case "doNotMix": + self = .donotmix + case "duckOthers": + self = .duckothers + case "auto": + self = .auto + default: + return nil + } + } + + /** + * Get the String value this MixAudioMode represents. + */ + var stringValue: String { + switch self { + case .mixwithothers: + return "mixWithOthers" + case .donotmix: + return "doNotMix" + case .duckothers: + return "duckOthers" + case .auto: + return "auto" + } + } +} diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/ExternalSubtitle.swift b/packages/react-native-video/nitrogen/generated/ios/swift/NativeExternalSubtitle.swift similarity index 52% rename from packages/react-native-video/nitrogen/generated/ios/swift/ExternalSubtitle.swift rename to packages/react-native-video/nitrogen/generated/ios/swift/NativeExternalSubtitle.swift index 1e91d7f4..8faad73c 100644 --- a/packages/react-native-video/nitrogen/generated/ios/swift/ExternalSubtitle.swift +++ b/packages/react-native-video/nitrogen/generated/ios/swift/NativeExternalSubtitle.swift @@ -1,5 +1,5 @@ /// -/// ExternalSubtitle.swift +/// NativeExternalSubtitle.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro /// Copyright © 2025 Marc Rousavy @ Margelo @@ -8,18 +8,18 @@ import NitroModules /** - * Represents an instance of `ExternalSubtitle`, backed by a C++ struct. + * Represents an instance of `NativeExternalSubtitle`, backed by a C++ struct. */ -public typealias ExternalSubtitle = margelo.nitro.video.ExternalSubtitle +public typealias NativeExternalSubtitle = margelo.nitro.video.NativeExternalSubtitle -public extension ExternalSubtitle { +public extension NativeExternalSubtitle { private typealias bridge = margelo.nitro.video.bridge.swift /** - * Create a new instance of `ExternalSubtitle`. + * Create a new instance of `NativeExternalSubtitle`. */ - init(uri: String, label: String) { - self.init(std.string(uri), std.string(label)) + init(uri: String, label: String, type: SubtitleType) { + self.init(std.string(uri), std.string(label), type) } var uri: String { @@ -43,4 +43,15 @@ public extension ExternalSubtitle { self.__label = std.string(newValue) } } + + var type: SubtitleType { + @inline(__always) + get { + return self.__type + } + @inline(__always) + set { + self.__type = newValue + } + } } diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/NativeVideoConfig.swift b/packages/react-native-video/nitrogen/generated/ios/swift/NativeVideoConfig.swift index e0e158df..27e17a17 100644 --- a/packages/react-native-video/nitrogen/generated/ios/swift/NativeVideoConfig.swift +++ b/packages/react-native-video/nitrogen/generated/ios/swift/NativeVideoConfig.swift @@ -18,8 +18,20 @@ public extension NativeVideoConfig { /** * Create a new instance of `NativeVideoConfig`. */ - init(uri: String, headers: Dictionary?, externalSubtitles: [ExternalSubtitle]?) { - self.init(std.string(uri), { () -> bridge.std__optional_std__unordered_map_std__string__std__string__ in + init(uri: String, externalSubtitles: [NativeExternalSubtitle]?, headers: Dictionary?) { + self.init(std.string(uri), { () -> bridge.std__optional_std__vector_NativeExternalSubtitle__ in + if let __unwrappedValue = externalSubtitles { + return bridge.create_std__optional_std__vector_NativeExternalSubtitle__({ () -> bridge.std__vector_NativeExternalSubtitle_ in + var __vector = bridge.create_std__vector_NativeExternalSubtitle_(__unwrappedValue.count) + for __item in __unwrappedValue { + __vector.push_back(__item) + } + return __vector + }()) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__unordered_map_std__string__std__string__ in if let __unwrappedValue = headers { return bridge.create_std__optional_std__unordered_map_std__string__std__string__({ () -> bridge.std__unordered_map_std__string__std__string_ in var __map = bridge.create_std__unordered_map_std__string__std__string_(__unwrappedValue.count) @@ -31,18 +43,6 @@ public extension NativeVideoConfig { } else { return .init() } - }(), { () -> bridge.std__optional_std__vector_ExternalSubtitle__ in - if let __unwrappedValue = externalSubtitles { - return bridge.create_std__optional_std__vector_ExternalSubtitle__({ () -> bridge.std__vector_ExternalSubtitle_ in - var __vector = bridge.create_std__vector_ExternalSubtitle_(__unwrappedValue.count) - for __item in __unwrappedValue { - __vector.push_back(__item) - } - return __vector - }()) - } else { - return .init() - } }()) } @@ -57,6 +57,35 @@ public extension NativeVideoConfig { } } + var externalSubtitles: [NativeExternalSubtitle]? { + @inline(__always) + get { + return { () -> [NativeExternalSubtitle]? in + if let __unwrapped = self.__externalSubtitles.value { + return __unwrapped.map({ __item in __item }) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__externalSubtitles = { () -> bridge.std__optional_std__vector_NativeExternalSubtitle__ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__vector_NativeExternalSubtitle__({ () -> bridge.std__vector_NativeExternalSubtitle_ in + var __vector = bridge.create_std__vector_NativeExternalSubtitle_(__unwrappedValue.count) + for __item in __unwrappedValue { + __vector.push_back(__item) + } + return __vector + }()) + } else { + return .init() + } + }() + } + } + var headers: Dictionary? { @inline(__always) get { @@ -93,33 +122,4 @@ public extension NativeVideoConfig { }() } } - - var externalSubtitles: [ExternalSubtitle]? { - @inline(__always) - get { - return { () -> [ExternalSubtitle]? in - if let __unwrapped = self.__externalSubtitles.value { - return __unwrapped.map({ __item in __item }) - } else { - return nil - } - }() - } - @inline(__always) - set { - self.__externalSubtitles = { () -> bridge.std__optional_std__vector_ExternalSubtitle__ in - if let __unwrappedValue = newValue { - return bridge.create_std__optional_std__vector_ExternalSubtitle__({ () -> bridge.std__vector_ExternalSubtitle_ in - var __vector = bridge.create_std__vector_ExternalSubtitle_(__unwrappedValue.count) - for __item in __unwrappedValue { - __vector.push_back(__item) - } - return __vector - }()) - } else { - return .init() - } - }() - } - } } diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/ResizeMode.swift b/packages/react-native-video/nitrogen/generated/ios/swift/ResizeMode.swift new file mode 100644 index 00000000..c596c061 --- /dev/null +++ b/packages/react-native-video/nitrogen/generated/ios/swift/ResizeMode.swift @@ -0,0 +1,48 @@ +/// +/// ResizeMode.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `ResizeMode`, backed by a C++ enum. + */ +public typealias ResizeMode = margelo.nitro.video.ResizeMode + +public extension ResizeMode { + /** + * Get a ResizeMode for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "contain": + self = .contain + case "cover": + self = .cover + case "stretch": + self = .stretch + case "none": + self = .none + default: + return nil + } + } + + /** + * Get the String value this ResizeMode represents. + */ + var stringValue: String { + switch self { + case .contain: + return "contain" + case .cover: + return "cover" + case .stretch: + return "stretch" + case .none: + return "none" + } + } +} diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/SubtitleType.swift b/packages/react-native-video/nitrogen/generated/ios/swift/SubtitleType.swift new file mode 100644 index 00000000..2e0eb2f6 --- /dev/null +++ b/packages/react-native-video/nitrogen/generated/ios/swift/SubtitleType.swift @@ -0,0 +1,52 @@ +/// +/// SubtitleType.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `SubtitleType`, backed by a C++ enum. + */ +public typealias SubtitleType = margelo.nitro.video.SubtitleType + +public extension SubtitleType { + /** + * Get a SubtitleType for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "auto": + self = .auto + case "vtt": + self = .vtt + case "srt": + self = .srt + case "ssa": + self = .ssa + case "ass": + self = .ass + default: + return nil + } + } + + /** + * Get the String value this SubtitleType represents. + */ + var stringValue: String { + switch self { + case .auto: + return "auto" + case .vtt: + return "vtt" + case .srt: + return "srt" + case .ssa: + return "ssa" + case .ass: + return "ass" + } + } +} diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/TextTrack.swift b/packages/react-native-video/nitrogen/generated/ios/swift/TextTrack.swift new file mode 100644 index 00000000..e2f74bdf --- /dev/null +++ b/packages/react-native-video/nitrogen/generated/ios/swift/TextTrack.swift @@ -0,0 +1,86 @@ +/// +/// TextTrack.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `TextTrack`, backed by a C++ struct. + */ +public typealias TextTrack = margelo.nitro.video.TextTrack + +public extension TextTrack { + private typealias bridge = margelo.nitro.video.bridge.swift + + /** + * Create a new instance of `TextTrack`. + */ + init(id: String, label: String, language: String?, selected: Bool) { + self.init(std.string(id), std.string(label), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = language { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), selected) + } + + var id: String { + @inline(__always) + get { + return String(self.__id) + } + @inline(__always) + set { + self.__id = std.string(newValue) + } + } + + var label: String { + @inline(__always) + get { + return String(self.__label) + } + @inline(__always) + set { + self.__label = std.string(newValue) + } + } + + var language: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__language.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__language = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var selected: Bool { + @inline(__always) + get { + return self.__selected + } + @inline(__always) + set { + self.__selected = newValue + } + } +} diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerEventEmitterSpec.cpp b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerEventEmitterSpec.cpp index b7c06882..09a17aa1 100644 --- a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerEventEmitterSpec.cpp +++ b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerEventEmitterSpec.cpp @@ -46,6 +46,8 @@ namespace margelo::nitro::video { prototype.registerHybridSetter("onTimedMetadata", &HybridVideoPlayerEventEmitterSpec::setOnTimedMetadata); prototype.registerHybridGetter("onTextTrackDataChanged", &HybridVideoPlayerEventEmitterSpec::getOnTextTrackDataChanged); prototype.registerHybridSetter("onTextTrackDataChanged", &HybridVideoPlayerEventEmitterSpec::setOnTextTrackDataChanged); + prototype.registerHybridGetter("onTrackChange", &HybridVideoPlayerEventEmitterSpec::getOnTrackChange); + prototype.registerHybridSetter("onTrackChange", &HybridVideoPlayerEventEmitterSpec::setOnTrackChange); prototype.registerHybridGetter("onVolumeChange", &HybridVideoPlayerEventEmitterSpec::getOnVolumeChange); prototype.registerHybridSetter("onVolumeChange", &HybridVideoPlayerEventEmitterSpec::setOnVolumeChange); prototype.registerHybridGetter("onStatusChange", &HybridVideoPlayerEventEmitterSpec::getOnStatusChange); diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerEventEmitterSpec.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerEventEmitterSpec.hpp index af097da4..abde50fc 100644 --- a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerEventEmitterSpec.hpp +++ b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerEventEmitterSpec.hpp @@ -25,6 +25,8 @@ namespace margelo::nitro::video { struct onPlaybackStateChangeData; } namespace margelo::nitro::video { struct onProgressData; } // Forward declaration of `TimedMetadata` to properly resolve imports. namespace margelo::nitro::video { struct TimedMetadata; } +// 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; } @@ -37,6 +39,8 @@ namespace margelo::nitro::video { enum class VideoPlayerStatus; } #include "TimedMetadata.hpp" #include #include +#include +#include "TextTrack.hpp" #include "VideoPlayerStatus.hpp" namespace margelo::nitro::video { @@ -98,6 +102,8 @@ namespace margelo::nitro::video { virtual void setOnTimedMetadata(const std::function& onTimedMetadata) = 0; virtual std::function& /* texts */)> getOnTextTrackDataChanged() = 0; virtual void setOnTextTrackDataChanged(const std::function& /* texts */)>& onTextTrackDataChanged) = 0; + virtual std::function& /* track */)> getOnTrackChange() = 0; + virtual void setOnTrackChange(const std::function& /* track */)>& onTrackChange) = 0; virtual std::function getOnVolumeChange() = 0; virtual void setOnVolumeChange(const std::function& onVolumeChange) = 0; virtual std::function getOnStatusChange() = 0; diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.cpp b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.cpp index 5ef5ab68..c3fc84e2 100644 --- a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.cpp +++ b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.cpp @@ -28,8 +28,19 @@ namespace margelo::nitro::video { prototype.registerHybridSetter("loop", &HybridVideoPlayerSpec::setLoop); prototype.registerHybridGetter("rate", &HybridVideoPlayerSpec::getRate); prototype.registerHybridSetter("rate", &HybridVideoPlayerSpec::setRate); + prototype.registerHybridGetter("mixAudioMode", &HybridVideoPlayerSpec::getMixAudioMode); + prototype.registerHybridSetter("mixAudioMode", &HybridVideoPlayerSpec::setMixAudioMode); + prototype.registerHybridGetter("ignoreSilentSwitchMode", &HybridVideoPlayerSpec::getIgnoreSilentSwitchMode); + prototype.registerHybridSetter("ignoreSilentSwitchMode", &HybridVideoPlayerSpec::setIgnoreSilentSwitchMode); + prototype.registerHybridGetter("playInBackground", &HybridVideoPlayerSpec::getPlayInBackground); + prototype.registerHybridSetter("playInBackground", &HybridVideoPlayerSpec::setPlayInBackground); + prototype.registerHybridGetter("playWhenInactive", &HybridVideoPlayerSpec::getPlayWhenInactive); + prototype.registerHybridSetter("playWhenInactive", &HybridVideoPlayerSpec::setPlayWhenInactive); prototype.registerHybridGetter("isPlaying", &HybridVideoPlayerSpec::getIsPlaying); + prototype.registerHybridGetter("selectedTrack", &HybridVideoPlayerSpec::getSelectedTrack); prototype.registerHybridMethod("replaceSourceAsync", &HybridVideoPlayerSpec::replaceSourceAsync); + prototype.registerHybridMethod("getAvailableTextTracks", &HybridVideoPlayerSpec::getAvailableTextTracks); + prototype.registerHybridMethod("selectTextTrack", &HybridVideoPlayerSpec::selectTextTrack); prototype.registerHybridMethod("clean", &HybridVideoPlayerSpec::clean); prototype.registerHybridMethod("preload", &HybridVideoPlayerSpec::preload); prototype.registerHybridMethod("play", &HybridVideoPlayerSpec::play); diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.hpp index e630afd5..a27e2939 100644 --- a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.hpp +++ b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.hpp @@ -19,13 +19,23 @@ 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" #include "HybridVideoPlayerEventEmitterSpec.hpp" #include "VideoPlayerStatus.hpp" -#include +#include "MixAudioMode.hpp" +#include "IgnoreSilentSwitchMode.hpp" #include +#include "TextTrack.hpp" +#include +#include namespace margelo::nitro::video { @@ -68,11 +78,22 @@ namespace margelo::nitro::video { virtual void setLoop(bool loop) = 0; virtual double getRate() = 0; virtual void setRate(double rate) = 0; + virtual MixAudioMode getMixAudioMode() = 0; + virtual void setMixAudioMode(MixAudioMode mixAudioMode) = 0; + virtual IgnoreSilentSwitchMode getIgnoreSilentSwitchMode() = 0; + virtual void setIgnoreSilentSwitchMode(IgnoreSilentSwitchMode ignoreSilentSwitchMode) = 0; + virtual bool getPlayInBackground() = 0; + virtual void setPlayInBackground(bool playInBackground) = 0; + virtual bool getPlayWhenInactive() = 0; + virtual void setPlayWhenInactive(bool playWhenInactive) = 0; virtual bool getIsPlaying() = 0; + virtual std::optional getSelectedTrack() = 0; public: // Methods virtual std::shared_ptr> replaceSourceAsync(const std::optional>& source) = 0; + virtual std::vector getAvailableTextTracks() = 0; + virtual void selectTextTrack(const std::optional& textTrack) = 0; virtual void clean() = 0; virtual std::shared_ptr> preload() = 0; virtual void play() = 0; diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoViewViewManagerSpec.cpp b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoViewViewManagerSpec.cpp index 8748622d..99f1667b 100644 --- a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoViewViewManagerSpec.cpp +++ b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoViewViewManagerSpec.cpp @@ -22,6 +22,8 @@ namespace margelo::nitro::video { prototype.registerHybridSetter("pictureInPicture", &HybridVideoViewViewManagerSpec::setPictureInPicture); prototype.registerHybridGetter("autoEnterPictureInPicture", &HybridVideoViewViewManagerSpec::getAutoEnterPictureInPicture); prototype.registerHybridSetter("autoEnterPictureInPicture", &HybridVideoViewViewManagerSpec::setAutoEnterPictureInPicture); + prototype.registerHybridGetter("resizeMode", &HybridVideoViewViewManagerSpec::getResizeMode); + prototype.registerHybridSetter("resizeMode", &HybridVideoViewViewManagerSpec::setResizeMode); prototype.registerHybridGetter("onPictureInPictureChange", &HybridVideoViewViewManagerSpec::getOnPictureInPictureChange); prototype.registerHybridSetter("onPictureInPictureChange", &HybridVideoViewViewManagerSpec::setOnPictureInPictureChange); prototype.registerHybridGetter("onFullscreenChange", &HybridVideoViewViewManagerSpec::getOnFullscreenChange); diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoViewViewManagerSpec.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoViewViewManagerSpec.hpp index 4aee1569..85524afa 100644 --- a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoViewViewManagerSpec.hpp +++ b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoViewViewManagerSpec.hpp @@ -15,10 +15,13 @@ // 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 "ResizeMode.hpp" #include namespace margelo::nitro::video { @@ -56,6 +59,8 @@ namespace margelo::nitro::video { virtual void setPictureInPicture(bool pictureInPicture) = 0; virtual bool getAutoEnterPictureInPicture() = 0; virtual void setAutoEnterPictureInPicture(bool autoEnterPictureInPicture) = 0; + virtual ResizeMode getResizeMode() = 0; + virtual void setResizeMode(ResizeMode resizeMode) = 0; virtual std::optional> getOnPictureInPictureChange() = 0; virtual void setOnPictureInPictureChange(const std::optional>& onPictureInPictureChange) = 0; virtual std::optional> getOnFullscreenChange() = 0; diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/IgnoreSilentSwitchMode.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/IgnoreSilentSwitchMode.hpp new file mode 100644 index 00000000..aeca6e13 --- /dev/null +++ b/packages/react-native-video/nitrogen/generated/shared/c++/IgnoreSilentSwitchMode.hpp @@ -0,0 +1,82 @@ +/// +/// IgnoreSilentSwitchMode.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::video { + + /** + * An enum which can be represented as a JavaScript union (IgnoreSilentSwitchMode). + */ + enum class IgnoreSilentSwitchMode { + AUTO SWIFT_NAME(auto) = 0, + IGNORE SWIFT_NAME(ignore) = 1, + OBEY SWIFT_NAME(obey) = 2, + } CLOSED_ENUM; + +} // namespace margelo::nitro::video + +namespace margelo::nitro { + + using namespace margelo::nitro::video; + + // C++ IgnoreSilentSwitchMode <> JS IgnoreSilentSwitchMode (union) + template <> + struct JSIConverter final { + static inline IgnoreSilentSwitchMode fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("auto"): return IgnoreSilentSwitchMode::AUTO; + case hashString("ignore"): return IgnoreSilentSwitchMode::IGNORE; + case hashString("obey"): return IgnoreSilentSwitchMode::OBEY; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum IgnoreSilentSwitchMode - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, IgnoreSilentSwitchMode arg) { + switch (arg) { + case IgnoreSilentSwitchMode::AUTO: return JSIConverter::toJSI(runtime, "auto"); + case IgnoreSilentSwitchMode::IGNORE: return JSIConverter::toJSI(runtime, "ignore"); + case IgnoreSilentSwitchMode::OBEY: return JSIConverter::toJSI(runtime, "obey"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert IgnoreSilentSwitchMode to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("auto"): + case hashString("ignore"): + case hashString("obey"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/MixAudioMode.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/MixAudioMode.hpp new file mode 100644 index 00000000..859c996a --- /dev/null +++ b/packages/react-native-video/nitrogen/generated/shared/c++/MixAudioMode.hpp @@ -0,0 +1,86 @@ +/// +/// MixAudioMode.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::video { + + /** + * An enum which can be represented as a JavaScript union (MixAudioMode). + */ + enum class MixAudioMode { + MIXWITHOTHERS SWIFT_NAME(mixwithothers) = 0, + DONOTMIX SWIFT_NAME(donotmix) = 1, + DUCKOTHERS SWIFT_NAME(duckothers) = 2, + AUTO SWIFT_NAME(auto) = 3, + } CLOSED_ENUM; + +} // namespace margelo::nitro::video + +namespace margelo::nitro { + + using namespace margelo::nitro::video; + + // C++ MixAudioMode <> JS MixAudioMode (union) + template <> + struct JSIConverter final { + static inline MixAudioMode fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("mixWithOthers"): return MixAudioMode::MIXWITHOTHERS; + case hashString("doNotMix"): return MixAudioMode::DONOTMIX; + case hashString("duckOthers"): return MixAudioMode::DUCKOTHERS; + case hashString("auto"): return MixAudioMode::AUTO; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum MixAudioMode - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, MixAudioMode arg) { + switch (arg) { + case MixAudioMode::MIXWITHOTHERS: return JSIConverter::toJSI(runtime, "mixWithOthers"); + case MixAudioMode::DONOTMIX: return JSIConverter::toJSI(runtime, "doNotMix"); + case MixAudioMode::DUCKOTHERS: return JSIConverter::toJSI(runtime, "duckOthers"); + case MixAudioMode::AUTO: return JSIConverter::toJSI(runtime, "auto"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert MixAudioMode to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("mixWithOthers"): + case hashString("doNotMix"): + case hashString("duckOthers"): + case hashString("auto"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/ExternalSubtitle.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/NativeExternalSubtitle.hpp similarity index 62% rename from packages/react-native-video/nitrogen/generated/shared/c++/ExternalSubtitle.hpp rename to packages/react-native-video/nitrogen/generated/shared/c++/NativeExternalSubtitle.hpp index e0735441..16af6022 100644 --- a/packages/react-native-video/nitrogen/generated/shared/c++/ExternalSubtitle.hpp +++ b/packages/react-native-video/nitrogen/generated/shared/c++/NativeExternalSubtitle.hpp @@ -1,5 +1,5 @@ /// -/// ExternalSubtitle.hpp +/// NativeExternalSubtitle.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro /// Copyright © 2025 Marc Rousavy @ Margelo @@ -18,23 +18,26 @@ #error NitroModules cannot be found! Are you sure you installed NitroModules properly? #endif - +// Forward declaration of `SubtitleType` to properly resolve imports. +namespace margelo::nitro::video { enum class SubtitleType; } #include +#include "SubtitleType.hpp" namespace margelo::nitro::video { /** - * A struct which can be represented as a JavaScript object (ExternalSubtitle). + * A struct which can be represented as a JavaScript object (NativeExternalSubtitle). */ - struct ExternalSubtitle { + struct NativeExternalSubtitle { public: std::string uri SWIFT_PRIVATE; std::string label SWIFT_PRIVATE; + SubtitleType type SWIFT_PRIVATE; public: - ExternalSubtitle() = default; - explicit ExternalSubtitle(std::string uri, std::string label): uri(uri), label(label) {} + NativeExternalSubtitle() = default; + explicit NativeExternalSubtitle(std::string uri, std::string label, SubtitleType type): uri(uri), label(label), type(type) {} }; } // namespace margelo::nitro::video @@ -43,20 +46,22 @@ namespace margelo::nitro { using namespace margelo::nitro::video; - // C++ ExternalSubtitle <> JS ExternalSubtitle (object) + // C++ NativeExternalSubtitle <> JS NativeExternalSubtitle (object) template <> - struct JSIConverter final { - static inline ExternalSubtitle fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + struct JSIConverter final { + static inline NativeExternalSubtitle fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { jsi::Object obj = arg.asObject(runtime); - return ExternalSubtitle( + return NativeExternalSubtitle( JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "uri")), - JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "label")) + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "label")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "type")) ); } - static inline jsi::Value toJSI(jsi::Runtime& runtime, const ExternalSubtitle& arg) { + static inline jsi::Value toJSI(jsi::Runtime& runtime, const NativeExternalSubtitle& arg) { jsi::Object obj(runtime); obj.setProperty(runtime, "uri", JSIConverter::toJSI(runtime, arg.uri)); obj.setProperty(runtime, "label", JSIConverter::toJSI(runtime, arg.label)); + obj.setProperty(runtime, "type", JSIConverter::toJSI(runtime, arg.type)); return obj; } static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { @@ -66,6 +71,7 @@ namespace margelo::nitro { jsi::Object obj = value.getObject(runtime); if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "uri"))) return false; if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "label"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "type"))) return false; return true; } }; diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/NativeVideoConfig.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/NativeVideoConfig.hpp index 6b351e1b..bf1db68f 100644 --- a/packages/react-native-video/nitrogen/generated/shared/c++/NativeVideoConfig.hpp +++ b/packages/react-native-video/nitrogen/generated/shared/c++/NativeVideoConfig.hpp @@ -18,14 +18,14 @@ #error NitroModules cannot be found! Are you sure you installed NitroModules properly? #endif -// 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; } #include #include -#include #include -#include "ExternalSubtitle.hpp" +#include "NativeExternalSubtitle.hpp" +#include namespace margelo::nitro::video { @@ -35,12 +35,12 @@ namespace margelo::nitro::video { struct NativeVideoConfig { public: std::string uri SWIFT_PRIVATE; + std::optional> externalSubtitles SWIFT_PRIVATE; std::optional> headers SWIFT_PRIVATE; - std::optional> externalSubtitles SWIFT_PRIVATE; public: NativeVideoConfig() = default; - explicit NativeVideoConfig(std::string uri, std::optional> headers, std::optional> externalSubtitles): uri(uri), headers(headers), externalSubtitles(externalSubtitles) {} + explicit NativeVideoConfig(std::string uri, std::optional> externalSubtitles, std::optional> headers): uri(uri), externalSubtitles(externalSubtitles), headers(headers) {} }; } // namespace margelo::nitro::video @@ -56,15 +56,15 @@ namespace margelo::nitro { jsi::Object obj = arg.asObject(runtime); return NativeVideoConfig( JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "uri")), - JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "headers")), - JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "externalSubtitles")) + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "externalSubtitles")), + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "headers")) ); } static inline jsi::Value toJSI(jsi::Runtime& runtime, const NativeVideoConfig& arg) { jsi::Object obj(runtime); obj.setProperty(runtime, "uri", JSIConverter::toJSI(runtime, arg.uri)); + obj.setProperty(runtime, "externalSubtitles", JSIConverter>>::toJSI(runtime, arg.externalSubtitles)); obj.setProperty(runtime, "headers", JSIConverter>>::toJSI(runtime, arg.headers)); - obj.setProperty(runtime, "externalSubtitles", JSIConverter>>::toJSI(runtime, arg.externalSubtitles)); return obj; } static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { @@ -73,8 +73,8 @@ namespace margelo::nitro { } jsi::Object obj = value.getObject(runtime); if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "uri"))) return false; + if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "externalSubtitles"))) return false; if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "headers"))) return false; - if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "externalSubtitles"))) return false; return true; } }; diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/ResizeMode.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/ResizeMode.hpp new file mode 100644 index 00000000..5eb82b60 --- /dev/null +++ b/packages/react-native-video/nitrogen/generated/shared/c++/ResizeMode.hpp @@ -0,0 +1,86 @@ +/// +/// ResizeMode.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::video { + + /** + * An enum which can be represented as a JavaScript union (ResizeMode). + */ + enum class ResizeMode { + CONTAIN SWIFT_NAME(contain) = 0, + COVER SWIFT_NAME(cover) = 1, + STRETCH SWIFT_NAME(stretch) = 2, + NONE SWIFT_NAME(none) = 3, + } CLOSED_ENUM; + +} // namespace margelo::nitro::video + +namespace margelo::nitro { + + using namespace margelo::nitro::video; + + // C++ ResizeMode <> JS ResizeMode (union) + template <> + struct JSIConverter final { + static inline ResizeMode fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("contain"): return ResizeMode::CONTAIN; + case hashString("cover"): return ResizeMode::COVER; + case hashString("stretch"): return ResizeMode::STRETCH; + case hashString("none"): return ResizeMode::NONE; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum ResizeMode - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, ResizeMode arg) { + switch (arg) { + case ResizeMode::CONTAIN: return JSIConverter::toJSI(runtime, "contain"); + case ResizeMode::COVER: return JSIConverter::toJSI(runtime, "cover"); + case ResizeMode::STRETCH: return JSIConverter::toJSI(runtime, "stretch"); + case ResizeMode::NONE: return JSIConverter::toJSI(runtime, "none"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert ResizeMode to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("contain"): + case hashString("cover"): + case hashString("stretch"): + case hashString("none"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/SubtitleType.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/SubtitleType.hpp new file mode 100644 index 00000000..c75a4ce1 --- /dev/null +++ b/packages/react-native-video/nitrogen/generated/shared/c++/SubtitleType.hpp @@ -0,0 +1,90 @@ +/// +/// SubtitleType.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::video { + + /** + * An enum which can be represented as a JavaScript union (SubtitleType). + */ + enum class SubtitleType { + AUTO SWIFT_NAME(auto) = 0, + VTT SWIFT_NAME(vtt) = 1, + SRT SWIFT_NAME(srt) = 2, + SSA SWIFT_NAME(ssa) = 3, + ASS SWIFT_NAME(ass) = 4, + } CLOSED_ENUM; + +} // namespace margelo::nitro::video + +namespace margelo::nitro { + + using namespace margelo::nitro::video; + + // C++ SubtitleType <> JS SubtitleType (union) + template <> + struct JSIConverter final { + static inline SubtitleType fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("auto"): return SubtitleType::AUTO; + case hashString("vtt"): return SubtitleType::VTT; + case hashString("srt"): return SubtitleType::SRT; + case hashString("ssa"): return SubtitleType::SSA; + case hashString("ass"): return SubtitleType::ASS; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum SubtitleType - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, SubtitleType arg) { + switch (arg) { + case SubtitleType::AUTO: return JSIConverter::toJSI(runtime, "auto"); + case SubtitleType::VTT: return JSIConverter::toJSI(runtime, "vtt"); + case SubtitleType::SRT: return JSIConverter::toJSI(runtime, "srt"); + case SubtitleType::SSA: return JSIConverter::toJSI(runtime, "ssa"); + case SubtitleType::ASS: return JSIConverter::toJSI(runtime, "ass"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert SubtitleType to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("auto"): + case hashString("vtt"): + case hashString("srt"): + case hashString("ssa"): + case hashString("ass"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/TextTrack.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/TextTrack.hpp new file mode 100644 index 00000000..8124a61c --- /dev/null +++ b/packages/react-native-video/nitrogen/generated/shared/c++/TextTrack.hpp @@ -0,0 +1,82 @@ +/// +/// 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 + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::video { + + /** + * A struct which can be represented as a JavaScript object (TextTrack). + */ + struct TextTrack { + public: + std::string id SWIFT_PRIVATE; + std::string label SWIFT_PRIVATE; + std::optional language SWIFT_PRIVATE; + bool selected SWIFT_PRIVATE; + + public: + TextTrack() = default; + explicit TextTrack(std::string id, std::string label, std::optional language, bool selected): id(id), label(label), language(language), selected(selected) {} + }; + +} // namespace margelo::nitro::video + +namespace margelo::nitro { + + using namespace margelo::nitro::video; + + // C++ TextTrack <> JS TextTrack (object) + template <> + struct JSIConverter final { + static inline TextTrack fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return TextTrack( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "id")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "label")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "language")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "selected")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const TextTrack& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "id", JSIConverter::toJSI(runtime, arg.id)); + obj.setProperty(runtime, "label", JSIConverter::toJSI(runtime, arg.label)); + obj.setProperty(runtime, "language", JSIConverter>::toJSI(runtime, arg.language)); + obj.setProperty(runtime, "selected", JSIConverter::toJSI(runtime, arg.selected)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "id"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "label"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "language"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "selected"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-video/package.json b/packages/react-native-video/package.json index 76c1425b..b7be27f2 100644 --- a/packages/react-native-video/package.json +++ b/packages/react-native-video/package.json @@ -1,6 +1,6 @@ { "name": "react-native-video", - "version": "7.0.0-dev.9", + "version": "7.0.0-dev.10", "description": "