Skip to content

Commit 03927e3

Browse files
authored
feat: improvements for ViewPortDetector (#136)
Fix excess rerenders Fix excess onchanged calls Fix not detecting when app goes into the background
1 parent 6382cba commit 03927e3

2 files changed

Lines changed: 78 additions & 33 deletions

File tree

src/components/VideoView.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
VideoTrack,
1010
} from 'livekit-client';
1111
import { RTCView } from '@livekit/react-native-webrtc';
12-
import { useEffect, useMemo, useState } from 'react';
12+
import { useCallback, useEffect, useMemo, useState } from 'react';
1313
import { RemoteVideoTrack } from 'livekit-client';
1414
import ViewPortDetector from './ViewPortDetector';
1515

@@ -35,6 +35,14 @@ export const VideoView = ({
3535
return info;
3636
});
3737

38+
const layoutOnChange = useCallback(
39+
(event: LayoutChangeEvent) => elementInfo.onLayout(event),
40+
[elementInfo]
41+
);
42+
const visibilityOnChange = useCallback(
43+
(isVisible: boolean) => elementInfo.onVisibility(isVisible),
44+
[elementInfo]
45+
);
3846
const shouldObserveVisibility = useMemo(() => {
3947
return (
4048
videoTrack instanceof RemoteVideoTrack && videoTrack.isAdaptiveStream
@@ -70,23 +78,15 @@ export const VideoView = ({
7078
}, [videoTrack, elementInfo]);
7179

7280
return (
73-
<View
74-
style={{ ...style, ...styles.container }}
75-
onLayout={(event) => {
76-
elementInfo.onLayout(event);
77-
}}
78-
>
81+
<View style={{ ...style, ...styles.container }} onLayout={layoutOnChange}>
7982
<ViewPortDetector
80-
onChange={(isVisible: boolean) => elementInfo.onVisibility(isVisible)}
83+
onChange={visibilityOnChange}
8184
style={styles.videoView}
8285
disabled={!shouldObserveVisibility}
86+
propKey={videoTrack}
8387
>
8488
<RTCView
85-
// eslint-disable-next-line react-native/no-inline-styles
86-
style={{
87-
flex: 1,
88-
width: '100%',
89-
}}
89+
style={styles.videoView}
9090
streamURL={mediaStream?.toURL() ?? ''}
9191
objectFit={objectFit}
9292
zOrder={zOrder}

src/components/ViewPortDetector.tsx

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
'use strict';
22

33
import React, { Component, PropsWithChildren } from 'react';
4-
import { View, ViewStyle } from 'react-native';
4+
import {
5+
AppState,
6+
AppStateStatus,
7+
NativeEventSubscription,
8+
View,
9+
ViewStyle,
10+
} from 'react-native';
511

612
const DEFAULT_DELAY = 1000;
713

@@ -10,6 +16,7 @@ export type Props = {
1016
style?: ViewStyle;
1117
onChange?: (isVisible: boolean) => void;
1218
delay?: number;
19+
propKey?: any;
1320
};
1421

1522
class TimeoutHandler {
@@ -60,71 +67,109 @@ export default class ViewPortDetector extends Component<
6067
private lastValue: boolean | null = null;
6168
private interval: TimeoutHandler | null = null;
6269
private view: View | null = null;
70+
private lastAppStateActive = false;
71+
private appStateSubscription: NativeEventSubscription | null = null;
6372

6473
constructor(props: Props) {
6574
super(props);
6675
this.state = { rectTop: 0, rectBottom: 0 };
6776
}
6877

6978
componentDidMount() {
79+
this.lastAppStateActive = AppState.currentState === 'active';
80+
this.appStateSubscription = AppState.addEventListener(
81+
'change',
82+
this.handleAppStateChange
83+
);
7084
if (this.hasValidTimeout(this.props.disabled, this.props.delay)) {
7185
this.startWatching();
7286
}
7387
}
7488

7589
componentWillUnmount() {
90+
this.appStateSubscription?.remove();
91+
this.appStateSubscription = null;
7692
this.stopWatching();
7793
}
7894

79-
hasValidTimeout(disabled?: boolean, delay?: number): boolean {
95+
hasValidTimeout = (disabled?: boolean, delay?: number): boolean => {
8096
let disabledValue = disabled ?? false;
8197
let delayValue = delay ?? DEFAULT_DELAY;
82-
return !disabledValue && delayValue > 0;
83-
}
98+
return (
99+
AppState.currentState === 'active' && !disabledValue && delayValue > 0
100+
);
101+
};
84102

85103
UNSAFE_componentWillReceiveProps(nextProps: Props) {
86104
if (!this.hasValidTimeout(nextProps.disabled, nextProps.delay)) {
87105
this.stopWatching();
88106
} else {
89-
this.lastValue = null;
107+
if (this.props.propKey !== nextProps.propKey) {
108+
this.lastValue = null;
109+
}
90110
this.startWatching();
91111
}
92112
}
113+
handleAppStateChange = (nextAppState: AppStateStatus) => {
114+
let nextAppStateActive = nextAppState === 'active';
115+
if (this.lastAppStateActive !== nextAppStateActive) {
116+
this.checkVisibility();
117+
}
118+
this.lastAppStateActive = nextAppStateActive;
119+
120+
if (!this.hasValidTimeout(this.props.disabled, this.props.delay)) {
121+
this.stopWatching();
122+
} else {
123+
this.startWatching();
124+
}
125+
};
93126

94-
private startWatching() {
127+
startWatching = () => {
95128
if (this.interval) {
96129
return;
97130
}
98-
this.interval = setIntervalWithTimeout(() => {
99-
if (!this.view) {
100-
return;
101-
}
102-
this.view.measure((_x, _y, width, height, _pageX, _pageY) => {
103-
this.checkInViewPort(width, height);
104-
});
105-
}, this.props.delay || DEFAULT_DELAY);
106-
}
131+
this.interval = setIntervalWithTimeout(
132+
this.checkVisibility,
133+
this.props.delay || DEFAULT_DELAY
134+
);
135+
};
107136

108-
private stopWatching() {
137+
stopWatching = () => {
109138
this.interval?.clear();
110139
this.interval = null;
111-
}
140+
};
112141

113-
private checkInViewPort(width?: number, height?: number) {
142+
checkVisibility = () => {
143+
if (!this.view) {
144+
return;
145+
}
146+
147+
if (AppState.currentState !== 'active') {
148+
this.updateVisibility(false);
149+
return;
150+
}
151+
152+
this.view.measure((_x, _y, width, height, _pageX, _pageY) => {
153+
this.checkInViewPort(width, height);
154+
});
155+
};
156+
checkInViewPort = (width?: number, height?: number) => {
114157
let isVisible: boolean;
115158
// Not visible if any of these are missing.
116159
if (!width || !height) {
117160
isVisible = false;
118161
} else {
119162
isVisible = true;
120163
}
164+
this.updateVisibility(isVisible);
165+
};
121166

167+
updateVisibility = (isVisible: boolean) => {
122168
if (this.lastValue !== isVisible) {
123169
this.lastValue = isVisible;
124170
this.props.onChange?.(isVisible);
125171
}
126-
}
127-
172+
};
128173
render() {
129174
return (
130175
<View

0 commit comments

Comments
 (0)