This is the note of what I did to implement a live streaming component with MJPG on React Native
Background
I implemented a live streaming feature with Motion JPG on React Native long ago. Then I needed to modify some things on it last week. Actually, using MJPG on React Native is not so straightforward and there is no common package for MJPG. I learned how to do it when I implemented it first and implemented a small component by myself. However, I forgot most of it when I tried to modify it. So I needed to do reverse-engineering of what I did last time. So I decided to make notes of it.
What I did
First of all, from the conversation on Github, we need react-native-webview
package to show a live-streaming mjpg.
https://gist.github.com/stefangordon/268b979afebf6e6f337f8c3555b4eb36
So the basic starting point looks like below.
export function LiveStreaminView() {
return (
<WebView
source={{ uri: "http://your_ip/stream.mjpg" }}
style={styles.image}
startInLoadingState
/>
);
}
const styles = StyleSheet.create({
image: {
width: screenshotWidth,
},
});
If you want to know what each prop exactly does, you can find the description here.
Then MJPG sometimes takes time to process the live streaming view but the WebView
component can’t re-render the view based on the loading progress. So I needed to re-render it based on how much the live streaming was loaded. Then I introduced a state to the previous component and used the onLoadProgress
API of WebView
to re-render it. It looked like below.
const loadingThreshold = Platform.OS === 'ios' ? 0.88 : 0.78;
export function LiveStreamingView() {
const [progress, setProgress] = useState(0);
return (
<WebView
source={{ uri: "http://your_ip/stream.mjpg" }}
style={styles.image}
startInLoadingState
onLoadProgress={({ nativeEvent }) => setProgress(nativeEvent.progress)}
renderLoading={() => (progress < loadingThreshold ? <LoadingComponent /> : <></>)}
/>
);
}
const styles = StyleSheet.create({
image: {
width: screenshotWidth,
},
});
One note here is that the progress never reaches out 100 and the final stable number would be different depending on the platform. In the above example, I put some example numbers in loadingThreshold
. It would change how big your MJPG is. So please check with yours and adjust the numbers.
I needed to take cases into consideration where the loading failed. Sometimes, the MJPG’s server would go wrong. I used another state and onError
API to handle the situation.
const loadingThreshold = Platform.OS === 'ios' ? 0.88 : 0.78;
export function LiveStreamingView() {
const [progress, setProgress] = useState(0);
const [isError, setIsError] = useState(false);
if (isError) return <ErrorComponent />
return (
<WebView
source={{ uri: "http://your_ip/stream.mjpg" }}
style={styles.image}
startInLoadingState
onLoadProgress={({ nativeEvent }) => setProgress(nativeEvent.progress)}
renderLoading={() => (progress < loadingThreshold ? <LoadingComponent /> : <></>)}
onError(() => setIsError(true))
/>
);
}
const styles = StyleSheet.create({
image: {
width: screenshotWidth,
},
});
Next, in my case, I wanted to disable the scroll interactions with the live-view component. It was so straightforward that I just added bounce
and scrollEnabled
props to WebView
component.
const loadingThreshold = Platform.OS === 'ios' ? 0.88 : 0.78;
export function LiveStreamingView() {
const [progress, setProgress] = useState(0);
const [isError, setIsError] = useState(false);
if (isError) return <ErrorComponent />
return (
<WebView
source={{ uri: "http://your_ip/stream.mjpg" }}
style={styles.image}
startInLoadingState
onLoadProgress={({ nativeEvent }) => setProgress(nativeEvent.progress)}
renderLoading={() => (progress < loadingThreshold ? <LoadingComponent /> : <></>)}
onError(() => setIsError(true))
bounces={false}
scrollEnabled={false}
/>
);
}
const styles = StyleSheet.create({
image: {
width: screenshotWidth,
},
});
Then I wanted to disable the pinch interactions with the live-streaming component. It was not as easy as disabling scroll. There is an API suitable for this purpose, which is setBuiltInZoomControls
.
But It doesn’t work on iOS. So I googled and found some conversations on it.
I needed to create a javascript snippet to disable pinch gesture and inject it to WebView
. My final output looks like below.
const loadingThreshold = Platform.OS === 'ios' ? 0.88 : 0.78;
const JAVASCRIPT_TO_DISABLE_ZOOM = `
(function() {
const meta = document.createElement('meta');
meta.setAttribute('content', 'width=${screenshotWidth}, user-scalable=no');
meta.setAttribute('name', 'viewport');
document.getElementsByTagName('head')[0].appendChild(meta);
})();
`;
export function LiveStreamingView() {
const [progress, setProgress] = useState(0);
const [isError, setIsError] = useState(false);
if (isError) return <ErrorComponent />
return (
<WebView
source={{ uri: "http://your_ip/stream.mjpg" }}
style={styles.image}
startInLoadingState
onLoadProgress={({ nativeEvent }) => setProgress(nativeEvent.progress)}
renderLoading={() => (progress < loadingThreshold ? <LoadingComponent /> : <></>)}
onError(() => setIsError(true))
bounces={false}
scrollEnabled={false}
setBuiltInZoomControls={false}
injectedJavaScript={JAVASCRIPT_TO_DISABLE_ZOOM}
onMessage={() => {}}
/>
);
}
const styles = StyleSheet.create({
image: {
width: screenshotWidth,
},
});
One note here is that you need to add onMessage
prop to WebView
even if it’s empty. It is mentioned in the docs and the conversation which I attached above as well. But I didn’t read it carefully and I got stuck here for a few hours hahaha
Be sure to set an
onMessage
handler, even if it's a no-op, or the code will not be run.
That’s it!