feat: implement enterPictureInPictureOnLeave prop for both platform (Android, iOS) (#3385)
* docs: enable Android PIP * chore: change comments * feat(android): implement Android PictureInPicture * refactor: minor refactor code and apply lint * fix: rewrite pip action intent code for Android14 * fix: remove redundant codes * feat: add isInPictureInPicture flag for lifecycle handling - activity provide helper method for same purpose, but this flag makes code simple * feat: add pipFullscreenPlayerView for makes PIP include video only * fix: add manifest value checker for prevent crash * docs: add pictureInPicture prop's Android guide * fix: sync controller visibility * refactor: refining variable name * fix: check multi window mode when host pause - some OS version call onPause on multi-window mode * fix: handling when onStop is called while in multi-window mode * refactor: enhance PIP util codes * fix: fix FullscreenPlayerView constructor * refactor: add enterPictureInPictureOnLeave prop and pip methods - remove pictureInPicture boolean prop - add enterPictureInPictureOnLeave boolean prop - add enterPictureInPicture method - add exitPictureInPicture method * fix: fix lint error * fix: prevent audio play in background without playInBackground prop * fix: fix onDetachedFromWindow * docs: update docs for pip * fix(android): sync pip controller with external controller state - for media session * fix(ios): fix pip active fn variable reference * refactor(ios): refactor code * refactor(android): refactor codes * fix(android): fix lint error * refactor(android): refactor android pip logics * fix(android): fix flickering issue when stop picture in picture * fix(android): fix import * fix(android): fix picture in picture with fullscreen mode * fix(ios): fix syntax error * fix(android): fix Fragment managed code * refactor(android): remove redundant override lifecycle * fix(js): add PIP type definition for codegen * fix(android): fix syntax * chore(android): fix lint error * fix(ios): fix enter background handler * refactor(ios): remove redundant code * fix(ios): fix applicationDidEnterBackground for PIP * fix(android): fix onPictureInPictureStatusChanged * fix(ios): fix RCTPictureInPicture * refactor(android): Ignore exception for some device ignore pip checker - some device ignore PIP availability check, so we need to handle exception to prevent crash * fix(android): add hideWithoutPlayer fn into Kotlin ver * refactor(android): remove redundant code * fix(android): fix pip ratio to be calculated with correct ratio value * fix(android): fix crash issue when unmounting in PIP mode * fix(android): fix lint error * Update android/src/main/java/com/brentvatne/react/VideoManagerModule.kt * fix(android): fix lint error * fix(ios): fix lint error * fix(ios): fix lint error * feat(expo): add android picture in picture config within expo plugin * fix: Replace Fragment with androidx.activity - remove code that uses Fragment, which is a tricky implementation * fix: fix lint error * fix(android): disable auto enter when player released * fix(android): fix event handler to check based on Activity it's bound to --------- Co-authored-by: jonghun <jonghun@toss.im> Co-authored-by: Olivier Bouillet <62574056+freeboub@users.noreply.github.com>
This commit is contained in:
@@ -409,6 +409,40 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
[setFullScreen],
|
||||
);
|
||||
|
||||
const enterPictureInPicture = useCallback(async () => {
|
||||
if (!nativeRef.current) {
|
||||
console.warn('Video Component is not mounted');
|
||||
return;
|
||||
}
|
||||
|
||||
const _enterPictureInPicture = () => {
|
||||
NativeVideoManager.enterPictureInPictureCmd(getReactTag(nativeRef));
|
||||
};
|
||||
|
||||
Platform.select({
|
||||
ios: _enterPictureInPicture,
|
||||
android: _enterPictureInPicture,
|
||||
default: () => {},
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const exitPictureInPicture = useCallback(async () => {
|
||||
if (!nativeRef.current) {
|
||||
console.warn('Video Component is not mounted');
|
||||
return;
|
||||
}
|
||||
|
||||
const _exitPictureInPicture = () => {
|
||||
NativeVideoManager.exitPictureInPictureCmd(getReactTag(nativeRef));
|
||||
};
|
||||
|
||||
Platform.select({
|
||||
ios: _exitPictureInPicture,
|
||||
android: _exitPictureInPicture,
|
||||
default: () => {},
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const save = useCallback((options: object) => {
|
||||
// VideoManager.save can be null on android & windows
|
||||
if (Platform.OS !== 'ios') {
|
||||
@@ -657,6 +691,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
setVolume,
|
||||
getCurrentPosition,
|
||||
setFullScreen,
|
||||
enterPictureInPicture,
|
||||
exitPictureInPicture,
|
||||
setSource,
|
||||
}),
|
||||
[
|
||||
@@ -670,6 +706,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
setVolume,
|
||||
getCurrentPosition,
|
||||
setFullScreen,
|
||||
enterPictureInPicture,
|
||||
exitPictureInPicture,
|
||||
setSource,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -193,6 +193,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
dismissFullscreenPlayer,
|
||||
setFullScreen,
|
||||
save: unsupported,
|
||||
enterPictureInPicture: unsupported,
|
||||
exitPictureInPicture: unsupported,
|
||||
restoreUserInterfaceForPictureInPictureStopCompleted: unsupported,
|
||||
nativeHtmlVideoRef: nativeRef,
|
||||
}),
|
||||
|
||||
@@ -5,6 +5,11 @@ export type ConfigProps = {
|
||||
* @default false
|
||||
*/
|
||||
enableNotificationControls?: boolean;
|
||||
/**
|
||||
* Apply configs to be able to use Picture-in-picture on Android.
|
||||
* @default false
|
||||
*/
|
||||
enableAndroidPictureInPicture?: boolean;
|
||||
/**
|
||||
* Whether to enable background audio feature.
|
||||
* @default false
|
||||
|
||||
31
src/expo-plugins/withAndroidPictureInPicture.ts
Normal file
31
src/expo-plugins/withAndroidPictureInPicture.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
AndroidConfig,
|
||||
withAndroidManifest,
|
||||
type ConfigPlugin,
|
||||
} from '@expo/config-plugins';
|
||||
|
||||
export const withAndroidPictureInPicture: ConfigPlugin<boolean> = (
|
||||
config,
|
||||
enableAndroidPictureInPicture,
|
||||
) => {
|
||||
return withAndroidManifest(config, (_config) => {
|
||||
if (!enableAndroidPictureInPicture) {
|
||||
return _config;
|
||||
}
|
||||
|
||||
const mainActivity = AndroidConfig.Manifest.getMainActivity(
|
||||
_config.modResults,
|
||||
);
|
||||
|
||||
if (!mainActivity) {
|
||||
console.warn(
|
||||
'AndroidManifest.xml is missing an <activity android:name=".MainActivity" /> element - skipping adding Picture-In-Picture related config.',
|
||||
);
|
||||
return _config;
|
||||
}
|
||||
|
||||
mainActivity.$['android:supportsPictureInPicture'] = 'true';
|
||||
|
||||
return _config;
|
||||
});
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import {type ConfigPlugin, createRunOncePlugin} from '@expo/config-plugins';
|
||||
import type {ConfigProps} from './@types';
|
||||
import {withNotificationControls} from './withNotificationControls';
|
||||
import {withAndroidExtensions} from './withAndroidExtensions';
|
||||
import {withAndroidPictureInPicture} from './withAndroidPictureInPicture';
|
||||
import {withAds} from './withAds';
|
||||
import {withBackgroundAudio} from './withBackgroundAudio';
|
||||
import {withPermissions} from '@expo/config-plugins/build/android/Permissions';
|
||||
@@ -21,6 +22,13 @@ const withRNVideo: ConfigPlugin<ConfigProps> = (config, props = {}) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (props.enableAndroidPictureInPicture) {
|
||||
config = withAndroidPictureInPicture(
|
||||
config,
|
||||
props.enableAndroidPictureInPicture,
|
||||
);
|
||||
}
|
||||
|
||||
if (props.androidExtensions != null) {
|
||||
config = withAndroidExtensions(config, props.androidExtensions);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface VideoManagerType {
|
||||
setFullScreenCmd: (reactTag: Int32, fullScreen: boolean) => Promise<void>;
|
||||
setSourceCmd: (reactTag: Int32, source?: UnsafeObject) => Promise<void>;
|
||||
setVolumeCmd: (reactTag: Int32, volume: number) => Promise<void>;
|
||||
enterPictureInPictureCmd: (reactTag: number) => Promise<void>;
|
||||
exitPictureInPictureCmd: (reactTag: number) => Promise<void>;
|
||||
save: (reactTag: Int32, option: UnsafeObject) => Promise<VideoSaveData>;
|
||||
getCurrentPosition: (reactTag: Int32) => Promise<Int32>;
|
||||
}
|
||||
|
||||
@@ -349,7 +349,7 @@ export interface VideoNativeProps extends ViewProps {
|
||||
preventsDisplaySleepDuringVideoPlayback?: boolean;
|
||||
preferredForwardBufferDuration?: Float; //ios, 0
|
||||
playWhenInactive?: boolean; // ios, false
|
||||
pictureInPicture?: boolean; // ios, false
|
||||
enterPictureInPictureOnLeave?: boolean; // default false
|
||||
ignoreSilentSwitch?: WithDefault<string, 'inherit'>; // ios, 'inherit'
|
||||
mixWithOthers?: WithDefault<string, 'inherit'>; // ios, 'inherit'
|
||||
rate?: Float;
|
||||
|
||||
@@ -252,7 +252,7 @@ export interface ReactVideoEvents {
|
||||
onLoadStart?: (e: OnLoadStartData) => void; //All
|
||||
onPictureInPictureStatusChanged?: (
|
||||
e: OnPictureInPictureStatusChangedData,
|
||||
) => void; //iOS
|
||||
) => void; //Android, iOS
|
||||
onPlaybackRateChange?: (e: OnPlaybackRateChangeData) => void; //All
|
||||
onVolumeChange?: (e: OnVolumeChangeData) => void; //Android, iOS
|
||||
onProgress?: (e: OnProgressData) => void; //All
|
||||
|
||||
@@ -19,5 +19,7 @@ export interface VideoRef {
|
||||
getCurrentPosition: () => Promise<number>;
|
||||
setFullScreen: (fullScreen: boolean) => void;
|
||||
setSource: (source?: ReactVideoSource) => void;
|
||||
enterPictureInPicture: () => void;
|
||||
exitPictureInPicture: () => void;
|
||||
nativeHtmlVideoRef?: RefObject<HTMLVideoElement>; // web only
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps {
|
||||
mixWithOthers?: EnumValues<MixWithOthersType>; // iOS
|
||||
muted?: boolean;
|
||||
paused?: boolean;
|
||||
pictureInPicture?: boolean; // iOS
|
||||
enterPictureInPictureOnLeave?: boolean;
|
||||
playInBackground?: boolean;
|
||||
playWhenInactive?: boolean; // iOS
|
||||
poster?: string | ReactVideoPoster; // string is deprecated
|
||||
|
||||
Reference in New Issue
Block a user