Why is the FullScreenHandler not setting the player over the screen on Android / React Native?

So currently I’m working on a React Native project when we make use of Bitmovin player as a video player, so setting up the fullscreenHandler, (which actually the custom handler I created is pretty much similar as default one’s behaviour) I realised the other components around the Bitmovin video player doesn’t hide once the fullscreen icon is toggled, instead the orientation is normally changed to landscape alongside with the other JSX components( such as Video details and video Flatlist) . so I was diving into the examples and there’s a snippet code that I think is where the layout must be updated by trying to get the parent view of the player however seems to not working or maybe I am doing the logic wrongly.

@Override
public void run() {
    if (CustomFullscreenHandler.this._playerView.getParent() instanceof ViewGroup) f
        ViewGroup parentView = (ViewGroup) CustomFullscreenHandler.this._playerView.getParent();

        for (int i = 0; i < parentView.getChildCount(); i++) {
            View child = parentView.getChildAt(i);

            if (child != _playerView) {
                child.setVisibility(fullscreen ? View.GONE: View.VISIBLE);
                child.getVisibility();
            }
        }
    }
}

I might dismiss many android concepts as I am not a native programmer so probably I could have a help through here.

Thanks in advance

1 Like

Hi @carrerafandres2496 ,

Thanks for you question. Which React Native wrapper are you using? Do you have code that I can run locally and reproduce it? A video or gif of the bug would also help understand what’s happening better.

I suspect you’re not using TakeOff’s wrapper, right? They seem to handle it here: react-native-bitmovin-player/ReactNativeBitmovinPlayerManager.java at 8dd31eecccc27b956f5d15efc63969f84cefbfae · take0ffmedia/react-native-bitmovin-player · GitHub

Community Tip

Try pasting the code directly using code tags instead of a screenshot as it’s easier for us to debug the issue:

```kotlin
[your code here]
```

I fixed it this time, but please going forward this will help us answer your question faster.

Hi @matheus.cardoso ,thanks for answer. Well you cought me a bit far from my laptop but as soon I’m back home, I will send you the code although I can give some highlights of the wrapper I’m using.

I built my own wrapper on top of the TakeOffmedia wrapper. It works fine the events and the actions from the fullscreen icon, however the actions gives the fullscreen effect but the other views are not hidden, it seems the views were been part of the videoPlayer view.

1 Like

Thanks for the quick response @carrerafandres2496.

I forgot to ask: which device are you testing on?

Also, what are you trying to achieve by forking the TakeOffMedia wrapper? Is your fork public and was the issue caused by your changes? If so, you could send me a link to it and I could test it and perhaps figure out what’s wrong.

Also, if your changes to the wrapper would benefit the main project, then it may be a good idea to send a PR after you’ve fixed the issues. This will ensure it will be maintained in the future and won’t cause conflicts with your fork.

We are doing the test on an android emulator (Pixel 3a API 30). I haven’t test it out on physical device though.

I am actually not forking the TakeOffMedia wrapper, I’m just using it as a base for building my own wrapper that actually is not public because it belongs to a company project. Moreover I could create a public repo on my GitHub with the wrapper with the action I want to achieve successfully.

Of course I could push a PR into the TakeOffMedia repo in order to maintain the repo :slight_smile: just that right now I am condition the repo to the needs of the video player that is needed and don’t have too much time to push a PR.

1 Like

@carrerafandres2496 then it’d be good to test it in a real device, and to help you further, I’d need you to send a project that simply reproduces the issue via DM to myself: @matheus.cardoso in a zip. You should remove any intellectual property and keep the flow simple. Ideally just the screen with the problem.

Hi @matheus.cardoso so I will send you the fragments codes and also a short video of showing the video. Many thanks!

Hi @carrerafandres2496,

The parent view of the PlayerView and its children might be a standalone container in your layout file. Meaning by “just” accessing the parent view, you might miss other layouts and/or fragments to hide.

Can you share your layout file that hosts the PlayerView?

Furthermore on Android (I don’t know exactly how this is handled in react-native) you have action bar and status bar, which are also different layouts. These would need to be invalidated/hidden as well to have a “real” fullscreen experience.

Hi @jarhoax, yes bellow you will find the layout that host the PlayerView which is basically pretty much similar to the TakeOffMedia’s component:

import React, { useEffect, useImperativeHandle, useRef, useState } from "react";
import {
    LayoutRectangle,
    NativeModules,
    Platform,
    View,
    findNodeHandle,
    requireNativeComponent,
} from "react-native";

const BitmovinVideoPlayerModule = NativeModules.BGBitmovinVideoPlayerModule;

export type ReactNativeBitmovinPlayerMethodsType = {
    play(): void;
    pause(): void;
    destroy(): void;
};

type BitmovinVideoPlayerType = {
    autoPlay: boolean;
    onReady?: (event: unknown) => void;
    onEnterFullscreen: (event: unknown) => void;
    configuration: {
        url: string;
        thumbnail?: string;
        title?: string;
        style?: {
            uiEnabled?: boolean;
            fullscreenIcon?: boolean;
        };
    };
    drmConfig?: {
        licenseUrl?: string | undefined;
        headers?: string | undefined;
        token?: string | undefined;
    };
    style?: any;
};
const BitmovinVideoPlayer = requireNativeComponent<BitmovinVideoPlayerType>(
    "BGBitmovinVideoPlayerManager",
);

export default React.forwardRef<
    ReactNativeBitmovinPlayerMethodsType,
    BitmovinVideoPlayerType
>(
    (
        {
            configuration,
            onEnterFullscreen,
            autoPlay,
            drmConfig,
            style,
        }: BitmovinVideoPlayerType,
        ref
    ) => {
        const [loading, setLoading] = useState(false);
        const [maxHeight, setMaxHeight] = useState<number | null>(null);
        const [layout, setLayout] = useState<LayoutRectangle | null>(null);
        const playerRef = useRef();
        
            // this need because video view stretched on initial render (RN 0.55.4)
            // TODO: check in future releases of RN
           
        const _onReady = React.useCallback(
            () => {
            console.log("layout: ",layout," masHight: ", maxHeight)
              if (Platform.OS === "android") {
                  if (layout && maxHeight === null) {
                      const { height } = layout;
                      setMaxHeight(height - 1);
                  }
              } 
            },
            [layout,maxHeight],
          )

        useEffect(() => {
            const { height } = layout || {};
            if ( maxHeight !== null && height && !loading) {
                setTimeout(() => {
                    setMaxHeight(height);
                }, 300);
                console.log("loading: ", loading)
            }
            if (height && maxHeight === height) {
                setLoading(true);
            }
        
        }, [maxHeight, layout, loading]);

        useEffect(() => {
            console.log("autoplay: " ,autoPlay)
            console.log("loading: ", loading)
            if (loading && autoPlay && Platform.OS === "android") {
                play();
                
            }
        }, [loading, autoPlay]);

        const play = () => {
            if (Platform.OS === "android") {
                BitmovinVideoPlayerModule.play(
                    findNodeHandle(playerRef?.current || null)
                );
            } else {
                BitmovinVideoPlayerModule.play();
            }
        };

        const pause = () => {
            if (Platform.OS === "android") {
                BitmovinVideoPlayerModule.pause(
                    findNodeHandle(playerRef?.current || null)
                );
            } else {
                BitmovinVideoPlayerModule.pause();
            }
        };

        const destroy = () => {
            if (Platform.OS === "android") {
                BitmovinVideoPlayerModule.destroy(
                    findNodeHandle(playerRef.current || null)
                );
            } else {
                BitmovinVideoPlayerModule.destroy();
            }
        };

        const mute = () => {
            BitmovinVideoPlayerModule.mute(
                findNodeHandle(playerRef.current || null)
            );
        };

        const unmute = () => {
            BitmovinVideoPlayerModule.unmute(
                findNodeHandle(playerRef.current || null)
            );
        };

        const enterFullscreen = () => {
            BitmovinVideoPlayerModule.enterFullscreen(
                findNodeHandle(playerRef.current || null)
            );
        };

        const exitFullscreen = () => {
            BitmovinVideoPlayerModule.exitFullscreen(
                findNodeHandle(playerRef.current || null)
            );
        };

        useImperativeHandle(ref, () => ({
            play,
            pause,
            destroy,
            mute,
            unmute,
            enterFullscreen,
            exitFullscreen,
        }));

        const _onEnterFullscreen = (event: any) => {
            enterFullscreen();
            if (onEnterFullscreen) {
                onEnterFullscreen(event);
            }
        };
       
        return (
            <View
                // eslint-disable-next-line react-native/no-inline-styles
                style={{ flex: 1 }}
                onLayout={(event) => {
                    setLayout(event?.nativeEvent.layout);
                }}>
                <BitmovinVideoPlayer
                    onEnterFullscreen={_onEnterFullscreen}
                    autoPlay={autoPlay}
                    ref={playerRef as any}
                    configuration={{
                        ...configuration,
                    }}
                    drmConfig={{
                        ...drmConfig,
                    }}
                    style={[
                        maxHeight != null
                            ? {
                                  maxHeight,
                              }
                            : null,
                        style,
                    ]}
                    onReady={_onReady}
                />
            </View>
        );
    }
);

The Custom fullscreen handler I’ve got so far is :

public class CustomFullscreenHandler implements FullscreenHandler {
    private final Activity _activity;
    private final PlayerView _playerView;
    private final View decorView;
    private boolean _isFullscreen;

    public CustomFullscreenHandler(Activity activity, PlayerView playerView) {
        this._activity = activity;
        this._playerView = playerView;
        this.decorView = activity.getWindow().getDecorView();
    }

    private void handleFullscreen(boolean fullscreen) {
        this._isFullscreen = fullscreen;
        this.doLayoutChanges(fullscreen);
        this.doSystemUiVisibility(fullscreen);
        this.windowsOrientation(fullscreen);
    }

    private void doSystemUiVisibility(final boolean fullscreen) {
        this.decorView.post(() -> {
            int uiParams = FullscreenUtil.getSystemUiVisibilityFlags(fullscreen, true);
            CustomFullscreenHandler.this.decorView.setSystemUiVisibility(uiParams);
        });

    }

    private void windowsOrientation(boolean fullScreen) {
        int fullScreen1 = this._activity.getWindowManager().getDefaultDisplay().getRotation();
        if (fullScreen) {
            if (fullScreen1 != 3) {
                this._activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            } else {
                this._activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
            }
        } else {
            this._activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
        }
    }
    private void doLayoutChanges(final boolean fullscreen) {
        Looper mainLooper = Looper.getMainLooper();
        boolean isAlreadyMainLooper = Looper.myLooper() == mainLooper;

        UpdateLayoutRunnable updateLayoutRunnable = new UpdateLayoutRunnable((AppCompatActivity) this._activity, fullscreen);

        if (isAlreadyMainLooper) {
            updateLayoutRunnable.run();
        } else {
            Handler handler = new Handler(mainLooper);
            handler.post(updateLayoutRunnable);
        }
    }

    private class UpdateLayoutRunnable implements Runnable {
        private final AppCompatActivity activity;
        private final boolean fullscreen;

        private UpdateLayoutRunnable(AppCompatActivity activity, boolean fullscreen) {
            this.activity = activity;
            this.fullscreen = fullscreen;
        }

        @Override
        public void run() {
            if (CustomFullscreenHandler.this._playerView.getParent() instanceof ViewGroup) {
                ViewGroup parentView = (ViewGroup) CustomFullscreenHandler.this._playerView.getParent();

                for (int i = 0; i < parentView.getChildCount(); i++) {
                    View child = parentView.getChildAt(i);

                    if (child != _playerView) {
                        child.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
                        child.getVisibility();
                    }
                }
            }
        }
    }

    public void onFullscreenRequested() {
        handleFullscreen(true);
    }

    public void onFullscreenExitRequested() {
        handleFullscreen(false);
    }

    public void onResume() {
        if (_isFullscreen) {
            doSystemUiVisibility(true);
        }
    }

    public void onPause() {
    }

    public void onDestroy() {
    }

    public boolean isFullscreen() {
        return this._isFullscreen;
    }

}

The JSX component where the BitmovinVideoPLayer is used:

const VideoComponent = () => {
    const [videoContext, setVideoContext] = useVideoContext();
    const [isLoaded, setLoaded] = useState(false);
    const navigation = useNavigation();
    const [orientation, setOrientation] = useState();
    const [token, setToken] = useState();
 
 
    const videoSource = useMemo(() => {
        return {
            uri: joinUrl(config.api.base_url, videoContext.root_urls.dash_url),
        };
    }, [videoContext.root_urls.dash_url]);

    const drmType = DRMType.WIDEVINE;
    const drmURL = joinUrl(config.api.base_url, "drm/acquire-widevine-license");

    const loadVideoListScreen = useCallback(() => {
        navigation.navigate("Home");
        setVideoContext((prevState) => ({
            ...prevState,
            uuid: "",
        }));
        return true;
    }, [navigation, setVideoContext]);

    return isLoaded ? (
        <View style={styles.container}>
        <BitmovinVideoPlayer
            style={styles.backgroundVideo}
            onEnterFullscreen={() => console.log("pressed fullscreen!!!!")}
            autoPlay={true}
            configuration={{
                url: videoSource.uri,
                title: "hello",
                style: {
                    fullscreenIcon: true,
                },
            }}
        />
        </View>
    ) : (
        <SafeAreaView style={styles.viewHeight}>
            <ActivityIndicator></ActivityIndicator>
        </SafeAreaView>
    );
};

const styles = StyleSheet.create({
    backgroundVideo: {
        bottom: 0,
        left: 0,
        position: "absolute",
        right: 0,
        top: 0,
    },
    container:{
        height:"30%",
        width:"100%"
    },
    iconClose: {
        alignItems: "flex-end",
    },
    linearGradient: {
        height: "25%",
        opacity: 0.75,
        position: "absolute",
        width: "100%",
    },
});

export default VideoComponent;

And this component is wrapped into a screen view which is handled by a navigation stack:

import React from "react";
import { StyleSheet, View } from "react-native";

import VideoComponent from "../components/videoComponent";
import VideoDetail from "../components/videoDetails";
import VideoFlatList from "../components/videoFlatList";
import { CategoryUUIDProvider } from "../context/categoryContext";
import { DataProvider } from "../context/dataContext";
import { PageProvider } from "../context/pageContext";

function VideoScreen() {
    const styles = StyleSheet.create({
        container: {
            alignItems: "flex-start",
            flex: 1,
            justifyContent: "flex-start",
        },
    });

    return (
        <CategoryUUIDProvider>
            <PageProvider>
                <DataProvider>
                    <View style={styles.container}>
                        <VideoComponent />
                        <VideoDetail />
                        <VideoFlatList />
                    </View>
                </DataProvider>
            </PageProvider>
        </CategoryUUIDProvider>
    );
}

export default VideoScreen;

And here I attach a gif of the issue:

ezgif.com-gif-maker (2)

Thanks for sharing!

Looking at your gif the handler pretty much seems to work. Only your videoListScreen seems to not be honored when hiding views. I also could not spot where and how const loadVideoListScreen is used or where its view is being rendered/returned.

I also have to say that my react-native knowledge is pretty limited but if you can get a reference to your video list and hide it in fullscreen that should be it. One hacky thing you could try is getting back via getParent() until you hit the rootView (so the Activity’s main view) and apply your fullscreen logic there.

FTR: I also was expecting something like a xml layout file you would have on native Android, but I honestly don’t know if that works on react-native.

Well, actually the loadVideoListScreenis not actually relevant as this is a function that was being part of a close button video functionality with the previous video player. Here is not implemented yet as I haven’t set properly the UI for the video player.

Yes I was actually trying a logic by using the onEnterFullscreen prop in order to change the state of the view styling in order to hide the rest but it seems those functions are not well set as it doesn’t catch properly any event, so I would need to have a look at that attempt.

in the other hand, I’ve also tried this by getting the getRootView(), method which return the in topmost view of the current view hierarchy, but this also hides the PlayerView letting a white background screen.

I see, thanks for pointing that out.

If you go the route with getRootView() and loop through it’s children its weird that it also hides the PlayerView as you exclude it with an equality check. It could be that the refs are different though, maybe try comparing them by their ids? Did you try that?

I have gone through the children but there are many of them :sweat_smile: and I don’t know until which top view the getRootView() I will try to placing a id for the view just to identify the proper children. Thanks, let you know how this goes. :slight_smile:

Hi @matheus.cardoso and @jarhoax , after applying the advices you suggested me, I finally could make it to work correctly, thanks for all :rocket:

Awesome @carrerafandres2496! Glad we could help :slight_smile: