Skip to content

Commit

Permalink
feat: improvements for ViewPortDetector (#136)
Browse files Browse the repository at this point in the history
Fix excess rerenders
Fix excess onchanged calls
Fix not detecting when app goes into the background
  • Loading branch information
davidliu committed Apr 10, 2024
1 parent 6382cba commit 03927e3
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 33 deletions.
26 changes: 13 additions & 13 deletions src/components/VideoView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
VideoTrack,
} from 'livekit-client';
import { RTCView } from '@livekit/react-native-webrtc';
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { RemoteVideoTrack } from 'livekit-client';
import ViewPortDetector from './ViewPortDetector';

Expand All @@ -35,6 +35,14 @@ export const VideoView = ({
return info;
});

const layoutOnChange = useCallback(
(event: LayoutChangeEvent) => elementInfo.onLayout(event),
[elementInfo]
);
const visibilityOnChange = useCallback(
(isVisible: boolean) => elementInfo.onVisibility(isVisible),
[elementInfo]
);
const shouldObserveVisibility = useMemo(() => {
return (
videoTrack instanceof RemoteVideoTrack && videoTrack.isAdaptiveStream
Expand Down Expand Up @@ -70,23 +78,15 @@ export const VideoView = ({
}, [videoTrack, elementInfo]);

return (
<View
style={{ ...style, ...styles.container }}
onLayout={(event) => {
elementInfo.onLayout(event);
}}
>
<View style={{ ...style, ...styles.container }} onLayout={layoutOnChange}>
<ViewPortDetector
onChange={(isVisible: boolean) => elementInfo.onVisibility(isVisible)}
onChange={visibilityOnChange}
style={styles.videoView}
disabled={!shouldObserveVisibility}
propKey={videoTrack}
>
<RTCView
// eslint-disable-next-line react-native/no-inline-styles
style={{
flex: 1,
width: '100%',
}}
style={styles.videoView}
streamURL={mediaStream?.toURL() ?? ''}
objectFit={objectFit}
zOrder={zOrder}
Expand Down
85 changes: 65 additions & 20 deletions src/components/ViewPortDetector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
'use strict';

import React, { Component, PropsWithChildren } from 'react';
import { View, ViewStyle } from 'react-native';
import {
AppState,
AppStateStatus,
NativeEventSubscription,
View,
ViewStyle,
} from 'react-native';

const DEFAULT_DELAY = 1000;

Expand All @@ -10,6 +16,7 @@ export type Props = {
style?: ViewStyle;
onChange?: (isVisible: boolean) => void;
delay?: number;
propKey?: any;
};

class TimeoutHandler {
Expand Down Expand Up @@ -60,71 +67,109 @@ export default class ViewPortDetector extends Component<
private lastValue: boolean | null = null;
private interval: TimeoutHandler | null = null;
private view: View | null = null;
private lastAppStateActive = false;
private appStateSubscription: NativeEventSubscription | null = null;

constructor(props: Props) {
super(props);
this.state = { rectTop: 0, rectBottom: 0 };
}

componentDidMount() {
this.lastAppStateActive = AppState.currentState === 'active';
this.appStateSubscription = AppState.addEventListener(
'change',
this.handleAppStateChange
);
if (this.hasValidTimeout(this.props.disabled, this.props.delay)) {
this.startWatching();
}
}

componentWillUnmount() {
this.appStateSubscription?.remove();
this.appStateSubscription = null;
this.stopWatching();
}

hasValidTimeout(disabled?: boolean, delay?: number): boolean {
hasValidTimeout = (disabled?: boolean, delay?: number): boolean => {
let disabledValue = disabled ?? false;
let delayValue = delay ?? DEFAULT_DELAY;
return !disabledValue && delayValue > 0;
}
return (
AppState.currentState === 'active' && !disabledValue && delayValue > 0
);
};

UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (!this.hasValidTimeout(nextProps.disabled, nextProps.delay)) {
this.stopWatching();
} else {
this.lastValue = null;
if (this.props.propKey !== nextProps.propKey) {
this.lastValue = null;
}
this.startWatching();
}
}
handleAppStateChange = (nextAppState: AppStateStatus) => {
let nextAppStateActive = nextAppState === 'active';
if (this.lastAppStateActive !== nextAppStateActive) {
this.checkVisibility();
}
this.lastAppStateActive = nextAppStateActive;

if (!this.hasValidTimeout(this.props.disabled, this.props.delay)) {
this.stopWatching();
} else {
this.startWatching();
}
};

private startWatching() {
startWatching = () => {
if (this.interval) {
return;
}
this.interval = setIntervalWithTimeout(() => {
if (!this.view) {
return;
}
this.view.measure((_x, _y, width, height, _pageX, _pageY) => {
this.checkInViewPort(width, height);
});
}, this.props.delay || DEFAULT_DELAY);
}
this.interval = setIntervalWithTimeout(
this.checkVisibility,
this.props.delay || DEFAULT_DELAY
);
};

private stopWatching() {
stopWatching = () => {
this.interval?.clear();
this.interval = null;
}
};

private checkInViewPort(width?: number, height?: number) {
checkVisibility = () => {
if (!this.view) {
return;
}

if (AppState.currentState !== 'active') {
this.updateVisibility(false);
return;
}

this.view.measure((_x, _y, width, height, _pageX, _pageY) => {
this.checkInViewPort(width, height);
});
};
checkInViewPort = (width?: number, height?: number) => {
let isVisible: boolean;
// Not visible if any of these are missing.
if (!width || !height) {
isVisible = false;
} else {
isVisible = true;
}
this.updateVisibility(isVisible);
};

updateVisibility = (isVisible: boolean) => {
if (this.lastValue !== isVisible) {
this.lastValue = isVisible;
this.props.onChange?.(isVisible);
}
}

};
render() {
return (
<View
Expand Down

0 comments on commit 03927e3

Please sign in to comment.