TheWidlarzGroup / react-native-video

A <Video /> component for react-native
https://docs.thewidlarzgroup.com/react-native-video/
MIT License
7.23k stars 2.9k forks source link

[BUG]: Resize mode cover with flat list #4166

Closed Rossella-Mascia-Neosyn closed 1 month ago

Rossella-Mascia-Neosyn commented 2 months ago

Version

6.5.0

What platforms are you having the problem on?

Android

System Version

14

On what device are you experiencing the issue?

Real device

Architecture

Old architecture

What happened?

when the resize mode and cover the video does not always fit

/* eslint-disable react-hooks/exhaustive-deps */
import {debounce} from 'lodash';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {Dimensions, FlatList, ListRenderItemInfo, StyleSheet, Text, View, ViewabilityConfig, ViewToken} from 'react-native';

import {PostProps} from '../../types/Post';
import Spinner from './Spinner';
import VideoItem from './VideoItem';

export const SCREEN_HEIGHT = Dimensions.get('window').height - 194;
export const SCREEN_WIDTH = Dimensions.get('window').width;

const SpinnerComponent = (loading: boolean) => (loading ? <Spinner /> : null);

export type FeedReelScrollProps = {
  fetchData: (offset: number, limit: number) => Promise<PostProps[]>;
  initialData: PostProps[];
  limit: number;
};

const FeedReelScroll: React.FC<FeedReelScrollProps> = ({fetchData, initialData, limit}) => {
  const [data, setData] = useState<PostProps[]>();
  const [offset, setOffset] = useState(0);
  const [isLoading, setIsLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const [currentVisibleIndex, setCurrentVisibleIndex] = useState(0);
  const [isRefreshing, setsRefreshing] = useState<boolean>(false);

  useEffect(() => {
    setData(initialData);
    setHasMore(true);
    setOffset(0);
  }, [initialData]);

  const viewabilityConfig = useRef<ViewabilityConfig>({
    itemVisiblePercentThreshold: 80,
  }).current;

  const onViewableItemsChanged = useRef(
    debounce(({viewableItems}: {viewableItems: ViewToken[]}) => {
      if (viewableItems.length > 0) {
        setCurrentVisibleIndex(viewableItems[0].index || 0);
      }
    }),
  ).current;

  const getItemLayout = useCallback(
    (data: ArrayLike<PostProps> | null | undefined, index: number) => ({
      length: SCREEN_HEIGHT,
      offset: SCREEN_HEIGHT * index,
      index,
    }),
    [],
  );

  const removeDuplicates = (originalData: PostProps[]): PostProps[] => {
    const uniqueDataMap = new Map();
    originalData?.forEach(item => {
      if (!uniqueDataMap.has(item.id)) {
        uniqueDataMap.set(item.id, item);
      }
    });
    return Array.from(uniqueDataMap.values());
  };

  const fetchFeed = useCallback(
    debounce(async (fetchOffset: number) => {
      if (isLoading || !hasMore) {
        return;
      }
      setIsLoading(true);
      try {
        const newData = await fetchData(fetchOffset, limit);
        setOffset(fetchOffset + limit);
        if (newData?.length < limit) {
          setHasMore(false);
        }
        setData(prev => removeDuplicates([...(prev || []), ...newData]));
      } catch (error) {
        console.error('fetchFeed: ', error);
      } finally {
        setIsLoading(false);
      }
    }, 200),
    [isLoading, hasMore, data],
  );

  const keyExtractor = useCallback((item: PostProps) => item.id.toString(), []);

  const renderVideoList = useCallback(
    ({index, item}: ListRenderItemInfo<PostProps>) => {
      return (
        <View style={styles.post} key={index}>
          <VideoItem isVisible={currentVisibleIndex === index} item={item} preload={Math.abs(currentVisibleIndex + 5) >= index} />
          <View style={styles.overlayComponent}>{item.overlayComponent}</View>
          <View style={styles.bodyContent}>{item.bodyContent}</View>
        </View>
      );
    },
    [currentVisibleIndex],
  );

  const memoizedValue = useMemo(() => renderVideoList, [currentVisibleIndex, data]);

  const onRefresh = async () => {
    setData(initialData);
    setHasMore(true);
    setOffset(0);
    setsRefreshing(true);
    try {
      await fetchFeed(offset);
    } catch (error) {
      console.error('On refresh:', error);
    } finally {
      setsRefreshing(false);
    }
  };

  return (
    <FlatList
      data={data || []}
      keyExtractor={keyExtractor}
      renderItem={memoizedValue}
      viewabilityConfig={viewabilityConfig}
      onViewableItemsChanged={onViewableItemsChanged}
      pagingEnabled
      windowSize={2}
      disableIntervalMomentum
      removeClippedSubviews
      initialNumToRender={1}
      maxToRenderPerBatch={2}
      onEndReachedThreshold={0.1}
      decelerationRate="normal"
      showsVerticalScrollIndicator={false}
      scrollEventThrottle={16}
      getItemLayout={getItemLayout}
      onEndReached={async () => {
        await fetchFeed(offset);
      }}
      // ListFooterComponent={SpinnerComponent(isLoading)}
      onRefresh={onRefresh}
      refreshing={isRefreshing}
      style={{flex: 1}}
    />
  );
};

const styles = StyleSheet.create({
  container: {
    width: SCREEN_WIDTH,
    height: SCREEN_HEIGHT,
  },
  post: {
    width: SCREEN_WIDTH,
    height: SCREEN_HEIGHT,
    position: 'relative',
  },
  overlayComponent: {
    position: 'absolute',
  },
  bodyContent: {
    position: 'absolute',
    top: 10,
    left: 10,
  },
});
export default FeedReelScroll;
import {useIsFocused} from '@react-navigation/native';
import React, {memo, useEffect, useState} from 'react';
import {Dimensions, Pressable, StyleSheet, TouchableHighlight, View} from 'react-native';
import FastImage from 'react-native-fast-image';
import Video from 'react-native-video';
import {PostProps} from '../../types/Post';

export type VideoItemProps = {
  item: PostProps;
  isVisible: boolean;
  preload: boolean;
};
export const SCREEN_HEIGHT = Dimensions.get('window').height - 196;
export const SCREEN_WIDTH = Dimensions.get('window').width;
const VideoItem: React.FC<VideoItemProps> = ({isVisible, item, preload}) => {
  const [paused, setPaused] = useState<string | null>(null);
  const [isPaused, setIsPaused] = useState(false);
  const [videoLoaded, setVideoLoaded] = useState(false);

  const isFocused = useIsFocused();

  useEffect(() => {
    setIsPaused(!isVisible);
    if (!isVisible) {
      setPaused(null);
      setVideoLoaded(false);
    }
  }, [isVisible]);

  useEffect(() => {
    if (!isFocused) {
      setIsPaused(true);
    }
    if (isFocused && isVisible) {
      setIsPaused(false);
    }
  }, [isFocused]);

  const handlerVideoLoad = () => {
    setVideoLoaded(true);
  };

  const _onPressPost = () => {
    const file = {
      id: item.id,
      source: item.video.source,
    };
    item.onPressPost && item.onPressPost(file);
  };

  return (
    <View style={styles.container}>
      <Pressable style={styles.videoContainer} onPress={_onPressPost}>
        {!videoLoaded && (
          <FastImage
            source={{
              uri: item.video.thumb,
              priority: FastImage.priority.high,
            }}
            resizeMode={FastImage.resizeMode.cover}
          />
        )}

        {isVisible || preload ? (
          <Video
            poster={item.video.poster}
            source={isVisible || preload ? {uri: item.video.source?.uri} : undefined}
            bufferConfig={{
              minBufferMs: 2500,
              maxBufferMs: 3000,
              bufferForPlaybackMs: 2500,
              bufferForPlaybackAfterRebufferMs: 2500,
              cacheSizeMB: 200,
            }}
            ignoreSilentSwitch="ignore"
            playWhenInactive={false}
            playInBackground={false}
            controls={false}
            disableFocus={true}
            style={styles.video}
            paused={isPaused}
            repeat
            hideShutterView
            minLoadRetryCount={5}
            resizeMode="cover"
            shutterColor="transparent"
            onReadyForDisplay={handlerVideoLoad}
          />
        ) : null}
      </Pressable>
    </View>
  );
};

const areEqual = (prevProps: VideoItemProps, nextProps: VideoItemProps) => {
  return prevProps.item.id === nextProps.item.id && prevProps.isVisible === nextProps.isVisible;
};

export default memo(VideoItem, areEqual);

const styles = StyleSheet.create({
  container: {
    position: 'relative',
    flex: 1,
  },
  video: {
    position: 'absolute',
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
  },
  videoContainer: {
    flex: 1,
  },
});

Reproduction Link

repository link

Reproduction

Step to reproduce this bug are:

github-actions[bot] commented 2 months ago

Thank you for your issue report. Please note that the following information is missing or incomplete:

Please update your issue with this information to help us address it more effectively.

Note: issues without complete information have a lower priority

freeboub commented 2 months ago

@Rossella-Mascia-Neosyn Can you please provide your sample code in a git repository ? It is very easier for reproduction ... Notice that I am currently working on optimizing this use case, I will have a look, I maybe already have a local fix

github-actions[bot] commented 2 months ago

Thank you for your issue report. Please note that the following information is missing or incomplete:

Please update your issue with this information to help us address it more effectively.

Note: issues without complete information have a lower priority

github-actions[bot] commented 2 months ago

Thank you for your issue report. Please note that the following information is missing or incomplete:

Please update your issue with this information to help us address it more effectively.

Note: issues without complete information have a lower priority

freeboub commented 2 months ago

@Rossella-Mascia-Neosyn Thank you for the sample, I am not sure to understand the issue, is this happening randomly while navigating between videos, or is it only on initial load ? I see something ugly on initial load, but not while navigating ... Maybe is it possible to have a video, it would be easier to understand the issue

freeboub commented 1 month ago

duplicates: https://github.com/TheWidlarzGroup/react-native-video/issues/4202

freeboub commented 1 month ago

I close this ticket, let's continue in https://github.com/TheWidlarzGroup/react-native-video/issues/4202