In the evolving landscape of video streaming technology, WebRTC has emerged as a powerful protocol for real-time communication. A popular, simple and open source WebRTC server is Janus implemented in C. It allows you to easily set up a performant streaming server on many different platforms including ESP, and combining it with a CAM module the camera can easily stream its video and after some configuration that stream can be accessible globally.
When we want our users to be able to access the streams globally, mobile clients seem like an obvious choice. It allows consumers to view the stream from anywhere, and modern mobile devices are performant enough to even view multiple streams simultaneously.
.
Planning
Network Considerations in Mobile Environments
Perhaps the most significant technical challenge facing mobile WebRTC clients is network variability. Desktop clients typically operate on stable networks with consistent bandwidth, while mobile devices regularly transition between network types and signal strengths.
A well-designed mobile client for Janus streaming must incorporate:
- Adaptive bitrate handling: Dynamically adjusting stream quality based on available bandwidth
- Connection recovery mechanisms: Gracefully managing stream reconnection when network transitions occur
- Bandwidth conservation: Implementing user-controlled quality settings to manage data usage
- Intelligent stream prioritization: Allocating available bandwidth to the most important camera views
Much of that functionality is already provided by Janus server and client gateway package, so keeping the mobile application in supported ecosystem is crucial for
User Experience Design for Mobile Viewing
The physical constraints of mobile devices demand different interface approaches than desktop applications. Effective mobile clients for multi-camera Janus streaming typically implement:
- Responsive grid layouts: Automatically adjusting camera arrangements based on device orientation
- Stream prioritization UI: Easy methods to designate primary and secondary views
- Touch-optimized controls: Gesture support for common actions like zooming or switching cameras
- Visual efficiency: High contrast UI elements that remain visible against variable video content
Technical Implementation Approaches
When developing mobile clients for WebRTC Janus streaming, developers typically choose between three implementation approaches:
- Web-based clients: HTML5 applications accessed through mobile browsers
- Hybrid applications: Web technologies wrapped in native containers
- Native applications: Platform-specific code using native WebRTC implementations
Each approach offers different tradeoffs. Web-based clients provide maximum deployment flexibility but limited access to device capabilities. Native applications offer optimal performance and deeper integration with mobile operating systems but require separate development for each platform. Hybrid approaches attempt to balance these considerations.
Performance testing reveals that native WebRTC implementations typically provide superior battery efficiency and more stable streaming performance, particularly when handling multiple simultaneous camera feeds.
However, this application’s most popular use case will be opening it only occasionally for checking cameras when something happens, and not having the app open for long periods of time. Because of that hybrid development provides seems as the best solution, because it still gives us some benefits of a native application, but makes developing it for iOS/Android simultaneously easier.
Frameworks for Hybrid applications
1.Flutter
The most performant popular framework for hybrid applications. Uses Dart and a special rendering engine to minimize the need to have platform specific functions. But even while being backed by Google it still has a relatively young and small ecosystem.
2. React Native
React Native leverages familiar JavaScript/TypeScript technology while rendering actual native UI components for better user experience. Its mature ecosystem includes extensive libraries and third-party packages. However, it can experience performance bottlenecks with complex animations and heavy loads, often necessitates platform-specific code for optimal experiences, and suffers from JavaScript bridge overhead.
3. Ionic
Ionic employs standard web technologies (HTML, CSS, and JavaScript/TypeScript) and works flexibly with Angular, React, Vue, or vanilla JavaScript. Its extensive UI component library adapts to platforms, and it offers excellent built-in Progressive Web App support. As a mature platform with strong documentation, Ionic enables the fastest development cycle among the three frameworks. However, being WebView-based limits its performance compared to Flutter and React Native, restricts access to native features, provides less native look and feel, relies heavily on Cordova/Capacitor plugins for native functionality, and can produce larger apps with slower startup times.
Summary
So while Flutter offers the best performance of all hybrid frameworks, and Ionic allows web developers minimal learning curve, React Native comes out on top, because it doesn’t require you to learn a whole new language with a relatively young and small ecosystem while also providing better performance and access to native functionality than Ionic.
System Resource Management
Video decoding is resource-intensive, making efficient system resource management critical for mobile clients displaying multiple streams. Technical considerations include:
- Hardware acceleration: Utilizing GPU capabilities for video decoding
- Memory management: Implementing buffer strategies appropriate for mobile constraints
- Background behavior: Managing streams when applications enter background state
The effective management of these resources directly impacts both user experience and practical usability. Mobile clients that fail to address these considerations often suffer from rapid battery depletion, device heating, and performance degradation over time.
Integration Capabilities
The utility of mobile clients extends beyond basic viewing functionality through integration with other systems and capabilities:
- Notification systems: Alerting users to events detected by video analytics
- Authentication systems: Securing access to sensitive camera feeds
- Storage systems: Facilitating the capture and secure storage of important footage
These integrations transform mobile clients from simple viewers into comprehensive tools that connect video streams with relevant contextual information and organizational workflows.
Conclusion
WebRTC technology through Janus offers powerful real-time video streaming to mobile devices, with React Native emerging as the optimal hybrid framework solution. This approach balances performance needs with development efficiency while leveraging Janus’s official support libraries.
Successful mobile streaming implementations must prioritize adaptive network handling, intuitive interfaces, and efficient resource management. By combining the Janus WebRTC server with a well-designed React Native client, organizations can effectively deliver real-time video content to users anywhere, maximizing the accessibility and utility of video monitoring systems.
Implementation
Janus support
Meetecho provides a janus-gateway package for easier integration with Janus WebRTC server using both REST api and WebSockets, but this package is specifically designed for use in browsers, and is just not compatible with web. There are modified versions of this package to provide compatibility with React Native:
- https://github.com/atyenoria/react-native-webrtc-janus-gateway
- https://github.com/chnirt/react-native-janus
- https://github.com/li-yanhao/janus-webrtc-react-native
But those solutions are not maintained for a long time (the newest commit of all those repositories is from 2020), so we have to handle the Janus server using react-native-webrtc.
React Native WebRTC
This package uses native modules for improved performance and smaller battery usage, but that comes with its own drawbacks. Native modules are harder to debug and require platform specific configuration. This application was developed only with Android in mind and might need more configuration to work with iOS, but the package itself supports iOS, so it should work.
Since this package uses native code it cannot be used with Expo Go app and overall requires additional configuration to work with Expo, I went the route of React Native CLI, so I only had to follow Android Installation Guide to get that package to work. Permissions are especially important as Android will throw a SecurityException when the app requests resources it doesn’t have permission to, and in my testing it will just crash the app without any logs.
Using Janus protocol in React Native WebRTC
Most information about how to use Janus without janus-gateway using Webosckets can be taken from https://janus.conf.meetecho.com/docs/rest.html#WS.
Context:
const [streamsList, setStreamsList] = useState([]);
const ws = useRef(null);
const sessionId = useRef(null)
const keepAliveInterval = useRef(null);
const peerConnection = useRef(null);
Useful helper functions:
const sendJanusMessage = useCallback(
(message: JanusMessage, callback?: (event: JanusEvent) => void) => {
if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
return false;
}
const janusMessage = {...message};
janusMessage.transaction = message.transaction ||
generateTransactionId();
if (callback) {
transactionCallbacks.current[janusMessage.transaction] = callback;
}
if (
janusMessage.session_id === undefined
&& sessionId.current !== null
) {
janusMessage.session_id = sessionId.current;
}
// message trickle and detach message types require handle_id
if (
janusMessage.handle_id === undefined &&
handleId.current !== null &&
['message', 'trickle', 'detach'].includes(janusMessage.janus)
) {
janusMessage.handle_id = handleId.current;
}
try {
ws.current.send(JSON.stringify(janusMessage));
return true;
} catch (error) {
return false;
}
},
[],
);
Setting up a WebSocket to view the stream from a Janus server can be simplified into these phases:
- Connecting to the server using janus-protocol (replace JANUS_WS_URL with address of your own server, by default its ws://localhost:8188) and creating new session
ws.current = new WebSocket(JANUS_WS_URL, 'janus-protocol');
ws.current.onopen = () => {
sendJanusMessage({janus: 'create'}, response => {
if (response.janus === 'success') {
// Store session ID in ref
sessionId.current = response.data.id;
setupKeepAlive();
attachPlugin();
} else {
Alert.alert(
'Connection Error',
'Failed to create a session with the Janus server',
);
}
});
}
2. Setup keepAlive calls and attach plugin
//set up keep aplive
keepAliveInterval.current = setInterval(() => {
sendJanusMessage({
janus: 'keepalive',
session_id: sessionId.current,
});
}, 50000);
//attach plugin
sendJanusMessage(
{
janus: 'attach',
session_id: sessionId.current,
plugin:'janus.plugin.streaming'
},
response => {
if (response.janus === 'success') {
handleId.current = response.data.id;
listStreams();
} else {
Alert.alert(
'Connection Error',
'Failed to connect to streaming plugin',
);
}
},
);
3. Get all available streams (we will need stream id to connect to one). For debugging purposes before adding some kind of a selector for stream it is possible to use a hardcoded streamID as they don’t change often, so just log the available streams before setStreamsList
sendJanusMessage(
{
janus: 'message',
session_id: sessionId.current,
handle_id: handleId.current,
body: {request: 'list'},
},
response => {
if (response.janus === 'success' && response.plugindata) {
const streams = response.plugindata.data.list || [];
if (streams.length > 0) {
setStreamsList(streams);
} else {
Alert.alert('Stream Error', 'No streams available');
}
} else {
Alert.alert('Stream Error', 'Failed to retrieve stream list');
}
},
);
4. Now when the WebSocket is connected, the session is ready and we have available stream ids, we have to select one of them and request ‘watch’, then wait for the SDP offer.
const watchRequest = {
janus: 'message',
session_id: sessionId.current,
handle_id: handleId.current,
body: {
request: 'watch',
id: streamId,
}
}
sendJanusMessage(watchRequest, response => {
if (response.janus === 'ack') {
// This is normal, Janus immediately sends an ACK, then the actual event later. Continue waiting for the actual event with stream info
return;
} else if (response.janus === 'event' && response.plugindata) {
if (response.plugindata.data.result === 'ok') {
// If there's a jsep object (SDP offer from the streaming plugin), handle it, otherwise keep waiting for the offer
if (response.jsep) {
handleRemoteJsep(response.jsep);
}
} else {
const errorMessage =
response.plugindata.data.error || 'Unknown error';
Alert.alert('Stream Error', errorMessage);
}
} else {
Alert.alert('Stream Error', 'unexpected server response')
}
})
5. Handle SDP offer
if (!peerConnection.current) {
const pc = new RTCPeerConnection(configuration);
peerConnection.current = pc;
// @ts-ignore - TypeScript doesn't recognize these properties correctly
pc.onicecandidate = (event: any) => {
if (event.candidate && sessionId.current && handleId.current) {
// Send ICE candidate to Janus
sendJanusMessage({
janus: 'trickle',
session_id: sessionId.current,
handle_id: handleId.current,
candidate: event.candidate,
});
} else if (!event.candidate) {
// All ICE candidates have been sent, send end-of-candidates
sendJanusMessage({
janus: 'trickle',
session_id: sessionId.current,
handle_id: handleId.current,
candidate: {completed: true},
});
}
};
// @ts-ignore - TypeScript doesn't recognize these properties correctly
pc.ontrack = (event: any) => {
if (event.streams && event.streams[0]) {
setRemoteStream(event.streams[0]);
}
};
}
// Set remote description (offer from Janus)
await peerConnection.current.setRemoteDescription(
new RTCSessionDescription(jsep),
);
// Create answer if we received an offer
if (jsep.type === 'offer') {
// Create answer - note: React Native WebRTC doesn't accept options directly
const answer = await peerConnection.current.createAnswer();
// Verify we got a valid answer before proceeding
if (!answer || !answer.sdp) {
return;
}
await peerConnection.current.setLocalDescription(answer);
if (sessionId.current && handleId.current) {
sendJanusMessage({
janus: 'message',
session_id: sessionId.current,
handle_id: handleId.current,
body: {request: 'start'},
jsep: peerConnection.current.localDescription,
});
}
}
Conclusion
There are more things that can be added, like handling for adaptive bitrate, but this is an MVP that can be used for showing to clients before making full implementation. It might use more battery right now and can cause the device to heat excessively, but those issues will be visible only on longer usage of the app. Another thing worth paying attention to is the fact that permissions provided in the guide for installing webrtc-react-native are more than necessary for most use cases, so as a good practice, it might be good to analyze what’s needed and what can be removed.