TheWidlarzGroup / react-native-video

A <Video /> component for react-native
http://thewidlarzgroup.github.io/react-native-video/
MIT License
7.14k stars 2.88k forks source link

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

Open Rossella-Mascia-Neosyn opened 6 days ago

Rossella-Mascia-Neosyn commented 6 days 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 6 days 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 3 days 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 days 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 days 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