How to implement Agora SDK videocall to react-native project - react-native

I successfully implemented the agora SDK videocall module with virtual background to my react.js web app, but when I try to implement it to the react-native mobile version I keep getting erros I don't know how to solve. I'm a bit new to react-native so this migth be an easy fix but I can't find it.
Basically, after submitting a form with the uid, channel, role, and token (I have a token service) the videocall component is rendered.
These are my dependencies
"agora-access-token": "^2.0.4",
"agora-react-native-rtm": "^1.5.0",
"agora-extension-virtual-background": "^1.1.1",
"agora-rtc-sdk-ng": "^4.14.0",
"agora-rn-uikit": "^4.0.0",
"axios": "^0.27.2",
"react": "18.0.0",
"react-native": "0.69.4",
"react-native-agora": "^3.7.1",
"react-native-dotenv": "^3.3.1"
This is the main videocall component.
import React,{ useEffect } from "react";
import { useState } from "react";
import axios from "axios";
import { Call } from "./components/Call";
const VideoCallApp = ({ videoCallData }) => {
const [token, setToken] = useState("");
const [virtualBackgroundData, setVirtualBackgroundData] = useState({
type: "img",
// example
// type: 'img',
// value: ''
//
// type: 'blur',
// value: integer // blurring degree, low (1), medium (2), or high (3).
//
// type: 'color',
// value: string // color on hex or string
});
useEffect(() => {
const getToken = async () => {
const url = `${process.env.REACT_APP_AGORA_TOKEN_SERVICE}/rtc/${videoCallData.channel}/${videoCallData.role}/uid/${videoCallData.uid}`;
try {
const response = await axios.get(url);
const token = response.data.rtcToken;
setToken(token);
} catch (err) {
alert(err);
}
};
getToken();
}, []);
return (
token && (
<Call
rtcProps={{
appId: process.env.REACT_APP_AGORA_APP_ID,
channel: videoCallData.channel,
token: token,
uid: videoCallData.uid,
}}
virtualBackground={virtualBackgroundData}
/>
)
);
};
export default VideoCallApp;
Which renders the Call component, it has more functionality for the virtual background.
import { useEffect, useState } from 'react'
import AgoraRTC from 'agora-rtc-sdk-ng';
import VirtualBackgroundExtension from 'agora-extension-virtual-background';
import { LocalVideo } from './LocalVideo';
import { RemoteVideo } from './RemoteVideo';
import { VideoControllers } from './VideoButtons';
import { View } from 'react-native';
const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
const extension = new VirtualBackgroundExtension();
AgoraRTC.registerExtensions([extension]);
export const Call = ({ rtcProps = {}, virtualBackground = {} }) => {
const [localTracks, setLocalTracks] = useState({
audioTrack: null,
videoTrack: null
});
const [processor, setProcessor] = useState(null);
useEffect(() => {
if (processor) {
try {
const initProcessor = async () => {
// Initialize the extension and pass in the URL of the Wasm file
await processor.init(process.env.PUBLIC_URL + "/assets/wasms");
// Inject the extension into the video processing pipeline in the SDK
localTracks.videoTrack.pipe(processor).pipe(localTracks.videoTrack.processorDestination);
playVirtualBackground();
}
initProcessor()
} catch (e) {
console.log("Fail to load WASM resource!"); return null;
}
}
}, [processor]);
useEffect(() => {
if (localTracks.videoTrack && processor) {
setProcessor(null);
}
}, [localTracks]);
const playVirtualBackground = async () => {
try {
switch (virtualBackground.type) {
case 'color':
processor.setOptions({ type: 'color', color: virtualBackground.value });
break;
case 'blur':
processor.setOptions({ type: 'blur', blurDegree: Number(virtualBackground.value) });
break;
case 'img':
const imgElement = document.createElement('img');
imgElement.onload = async () => {
try {
processor.setOptions({ type: 'img', source: imgElement });
await processor.enable();
} catch (error) {
console.log(error)
}
}
imgElement.src = process.env.PUBLIC_URL + '/assets/backgrounds/background-7.jpg';
imgElement.crossOrigin = "anonymous";
break;
default:
break;
}
await processor.enable();
} catch (error) {
console.log(error)
}
}
const join = async () => {
await client.join(rtcProps.appId, rtcProps.channel, rtcProps.token, Number(rtcProps.uid));
}
const startVideo = () => {
AgoraRTC.createCameraVideoTrack()
.then(videoTrack => {
setLocalTracks(tracks => ({
...tracks,
videoTrack
}));
client.publish(videoTrack);
videoTrack.play('local');
})
}
const startAudio = () => {
AgoraRTC.createMicrophoneAudioTrack()
.then(audioTrack => {
setLocalTracks(tracks => ({
...tracks,
audioTrack
}));
client.publish(audioTrack);
});
}
const stopVideo = () => {
localTracks.videoTrack.close();
localTracks.videoTrack.stop();
client.unpublish(localTracks.videoTrack);
}
const stopAudio = () => {
localTracks.audioTrack.close();
localTracks.audioTrack.stop();
client.unpublish(localTracks.audioTrack);
}
const leaveVideoCall = () => {
stopVideo();
stopAudio();
client.leave();
}
async function startOneToOneVideoCall() {
join()
.then(() => {
startVideo();
startAudio();
client.on('user-published', async (user, mediaType) => {
if (client._users.length > 1) {
client.leave();
alert('Please Wait Room is Full');
return;
}
await client.subscribe(user, mediaType);
if (mediaType === 'video') {
const remoteVideoTrack = user.videoTrack;
remoteVideoTrack.play('remote');
}
if (mediaType === 'audio') {
user.audioTrack.play();
}
});
});
}
// Initialization
function setProcessorInstance() {
if (!processor && localTracks.videoTrack) {
// Create a VirtualBackgroundProcessor instance
setProcessor(extension.createProcessor());
}
}
async function setBackground() {
if (localTracks.videoTrack) {
setProcessorInstance()
}
}
useEffect(() => {
startOneToOneVideoCall();
}, []);
return (
<View >
<View>
<LocalVideo />
<RemoteVideo />
<VideoControllers
actions={{
startAudio,
stopAudio,
startVideo,
stopVideo,
leaveVideoCall,
startOneToOneVideoCall,
setBackground
}}
/>
</View>
</View>
)
}
The local an remote video component are emty Views where the videos are displayed and the VideoControllers are Buttons that manage the videocall.
When I run the app the form works fine but as soon as I subbmit it the app crashes with these errors.
WARN `new NativeEventEmitter()` was called with a non-null argument without the required `addListener` method.
WARN `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method.
LOG Running "videocall" with {"rootTag":1}
ERROR TypeError: window.addEventListener is not a function. (In 'window.addEventListener("online", function () {
_this32.networkState = EB.ONLINE;
})', 'window.addEventListener' is undefined)
VideoCallApp
Form#http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=com.videocall&modulesOnly=false&runModule=true:121056:41
RCTView
View
RCTView
View
AppContainer#http://localhost:8081/index.bundle?platform=android&dev=true&minify=false&app=com.videocall&modulesOnly=false&runModule=true:78626:36
videocall(RootComponent)
ERROR TypeError: undefined is not an object (evaluating '_$$_REQUIRE(_dependencyMap[5], "./components/Call").Call')
Something is happening at the Call component and I think it migth be the DOM manipulation for the videos but I can't find an example of a react-native project with agora SDK.
I don't want to use the UIkit because, eventhough it works, I can't use the virtual background which I need for this project. Can anyone help me?

Related

Multiple useEffect in react-native to achieve mentioned functionality

I need help with the async nature of Async storage and axios api. Here's the functionality that I am trying to achieve ->
send request to two separate api to get some data.
display that data on the screen with some additional text
api request are authenticated so a token is passed as Authentication Header
I have attached the current implementation, I am having the a number of errors in this
Errors:
Login_token not set in state after fetching from Async Storage.
Data not set in state after api call
both resulting in either failed api calls or undefined state errors on render
This is my code.
import React, { FunctionComponent, useEffect, useCallback, useState} from 'react';
import { StyleSheet, View} from 'react-native';
// chat
import { GiftedChat } from 'react-native-gifted-chat';
// navigation
import { RootStackParamList } from '../../navigators/RootStack';
import { StackScreenProps } from '#react-navigation/stack';
export type Props = StackScreenProps<RootStackParamList, "Chat">;
// api
import { Convo_details, Send_Msg, List_Msg, Expert_Public_Profile } from '../../api/UserApi';
import Spinner from 'react-native-loading-spinner-overlay';
import AsyncStorage from '#react-native-async-storage/async-storage';
import uuid from 'react-native-uuid';
const Chat: FunctionComponent<Props> = ({ navigation, route, ...props }) => {
// console.log(props.route.params);
const [login_token, setlogin_token] = useState('')
const [conversation_id, setconversation_id] = useState('')
const [conversation_details, setconversation_details] = useState({})
const [currentuser, setcurrentuser] = useState({})
const [loading, setLoading] = useState(false);
const [expertuid, setexpertuid] = useState('')
const [ExpertProfile, setExpertProfile] = useState({})
const [messages, setMessages] = useState([]);
useEffect(() => {
getlogintoken()
console.log("####################################","getlogintoken");
}, [])
/* conversationid */
useEffect(() => {
if (route.params != null) {
setconversation_id(route.params[0])
}
console.log("####################################","conversation id");
}, [])
/* expert uid */
useEffect(() => {
if (route.params != null) {
setexpertuid(route.params[1])
}
console.log("####################################","expert uid");
}, [])
/* expert public profile */
useEffect(() => {
getexpertpublicprofile()
getConvo_details()
console.log("####################################","convo_details");
}, [])
useEffect(() => {
// get current user
AsyncStorage.getItem("currentuser").then(res => {
if (res != null) setcurrentuser(res)
else alert("Current user not found")
})
console.log("####################################","current user");
}, [])
// set welcome msg
useEffect(() => {
if (Object.keys(conversation_details).length != 0 && Object.keys(ExpertProfile).length != 0)
setwelcomemsg()
}, [])
const onSend = useCallback(async (messages = []) => {
// console.log(messages[0].text);
setMessages(previousMessages => GiftedChat.append(previousMessages, messages))
const data = {
conversation_id: "f98d6851-a713-4f58-9118-77a779ff175f",//conversation_id,
message_type: "TEXT",
body: messages[0].text
}
const res: any = await Send_Msg(data, login_token)
.catch(error => {
alert(`Send_Msg -> ${error}`)
console.log(error);
return
})
if (res.status == 200) {
console.log(res.data);
} else console.log(res);
}, [])
const getexpertpublicprofile = async () => {
setLoading(true)
const res: any = await Expert_Public_Profile(expertuid, login_token)
.catch(error => {
setLoading(false)
console.log("Expert public profile ->");
alert(`Expert public profile ->${error.message}`)
console.log(error);
return
})
setLoading(false)
if (res.status === 200) setExpertProfile(res.data)
else {
alert(`get expert public profile${res.data.message}`)
console.log("getexpertpublicprofile -->");
console.log(res.data);
}
}
const getlogintoken = () => {
AsyncStorage.getItem("login_token").then(res => {
if (res != null) {
setLoading(false)
setlogin_token(res)
}
else alert("No login token found")
})
}
const getConvo_details = async () => {
setLoading(true)
const res: any = await Convo_details(conversation_id, login_token)
.catch(error => {
setLoading(false)
alert(`Convo_details-->${error.message}`)
console.log("Convo_details -->");
console.log(error);
return
})
setLoading(false)
if (res.status === 200) setconversation_details(res.data)
else {
alert(`get convo details-> ${res.data.message}`)
console.log("getConvo_details -->");
console.log(res.data);
}
}
const setwelcomemsg = () => {
try {
let user = JSON.parse(currentuser)
let messages = [
{
_id: uuid.v4().toString(),
conversation_id: conversation_details.conversation_id,
created_at: new Date(),
from: conversation_details.recipient.user_uid,
type: "TEXT",
text: `About Me - ${ExpertProfile.bio}`,
user: {
_id: conversation_details.recipient.user_uid,
}
},
{
_id: uuid.v4().toString(),
conversation_id: conversation_details.conversation_id,
created_at: new Date(),
from: conversation_details.recipient.user_uid,
type: "TEXT",
text: `My name is ${conversation_details.recipient.name}`,
user: {
_id: conversation_details.recipient.user_uid,
}
},
{
_id: uuid.v4().toString(),
conversation_id: conversation_details.conversation_id,
created_at: new Date(),
from: conversation_details.recipient.user_uid,
type: "TEXT",
text: `Hi ${user.full_name}`,
user: {
_id: conversation_details.recipient.user_uid,
}
}]
setMessages(previousMessages => GiftedChat.append(previousMessages, messages))
} catch (error) {
console.log("try -> set welcome msg");
console.log(error);
return
}
}
return (
<View style={styles.maincontainer}>
<Spinner
visible={loading}
textContent={'Loading...'}
textStyle={{ color: '#FFF' }}
/>
<GiftedChat
messages={messages}
onSend={messages => onSend(messages)}
user={{
_id: currentuser.user_uid,
}}
isTyping={false}
scrollToBottom={true}
showAvatarForEveryMessage={true}
renderAvatar={() => null}
/>
</View>
);
}
export default Chat;
const styles = StyleSheet.create({
maincontainer: {
flex: 1,
},
});
When axios returns, it usually give the response as res.data, so in your case, try either res.data or res.data.yourToken (I'm not sure how it's your object).
Gurav,
As far as your code above, The api call's will trigger even before you get currentuser or loginToken. You have to handle the api call after getting the currentuser and loginToken. This can be gracefully handled with async, await.
example code:
useEffect(() => {
getData()
}, [])
useEffect(() => {
if(login_token && currentuser) {
//The api call goes here after you get the logintoken andcurrentuser.
// The above condition is just an example but will vary based on your requirements
}
}, [login_token, currentuser])
const getData = async () => {
await getlogintoken()
await getcurrentuser()
}
const getlogintoken = async () => {
await AsyncStorage.getItem("login_token").then(res => {
if (res != null) {
setLoading(false)
setlogin_token(res)
}
else alert("No login token found")
})
}
const getcurrentuser = async () => {
await AsyncStorage.getItem("currentuser").then(res => {
if (res != null) setcurrentuser(res)
else alert("Current user not found")
})
}

Expo-AV Audio Error When Using CreateAsync URI. How can I fix?

I am receiving the following error message when loading a url mp3 file using expo-av in React Native.
Possible Unhandled Promise Rejection (id: 2):
Error: The AVPlayerItem instance has failed with the error code -1004 and domain "NSURLErrorDomain".```
Here is a simplified version of my code:
import { Text, View } from 'react-native';
import React, { useState, useEffect } from 'react';
import { Audio } from 'expo-av';
const AudioPlayer = ({ audioURL }) => {
const [playbackInstance, setPlaybackInstance] = useState();
const [playbackInstanceInfo, setPlaybackInstanceInfo] = useState({
position: 0,
duration: 0,
state: 'Buffering',
});
async function togglePlay() {
if (playbackInstanceInfo.state === 'Playing') {
setPlaybackInstanceInfo({
...playbackInstanceInfo,
state: 'Paused',
});
await playbackInstance.pauseAsync();
} else {
setPlaybackInstanceInfo({
...playbackInstanceInfo,
state: 'Play',
});
await playbackInstance.playAsync();
}
}
const onPlaybackStatusUpdate = status => {
if (status.isLoaded) {
setPlaybackInstanceInfo({
...playbackInstanceInfo,
position: status?.positionMillis,
duration: status?.durationMillis || 0,
state: status.didJustFinish
? 'Ended'
: status.isBuffering
? 'Buffering'
: status.shouldPlay
? 'Playing'
: 'Paused',
});
} else {
if (status.isLoaded === false && status.error) {
const errorMsg = `Encountered a fatal error
during playback: ${status.error}`;
console.log(errorMsg, 'error');
}
}
};
useEffect(() => {
const loadAudio = async () => {
const { sound } = await Audio.Sound.createAsync(
{ uri: audioURL },
{ shouldPlay: false },
onPlaybackStatusUpdate
);
setPlaybackInstance(sound);
};
loadAudio();
}, []);
useEffect(() => {
return playbackInstance
? () => {
playbackInstance.unloadAsync();
}
: undefined;
}, [playbackInstance]);
return (
<View></View>
)};
export default AudioPlayer;
Dependency Versions:
"expo": "^46.0.0",
"expo-av": "~12.0.4",
"react-native": "0.69.6"
I tried to delete node modules and package-lock.json, then reinstall, but that didn't help. I tried to update expo-av to 13.0.1 but I get a warning telling me that some dependencies I have are not compatible with it, so just went back to ~12.0.4. I'm using Node.js 15.0.1 with Express ^4.17.1

How can I test my custom react hook that uses fetch?

I have created a custom react hook which uses fetch from whatwg-fetch. I have tests for the components that make use of the hook and can mock the whole hook, but now am trying to write tests for the hook itself and my goal is to mock the fetch response. This is my hook.
import { useState, useEffect } from "react";
import "whatwg-fetch";
export default function useFetch(url) {
const [data, setData] = useState(undefined);
const [response, setResponse] = useState(undefined)
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState(undefined);
useEffect(() => {
try {
const fetchData = async () => {
const result = await fetch(url);
setResponse(result);
const responseText = await result.text();
setData(responseText);
setLoading(false);
};
fetchData();
} catch (error) {
setError(error);
}
}, [url]);
return { data, response, isLoading, error };
}
export { useFetch }
Currently, this is how my test looks like. Feels like I cannot mock the fetch to return the desired value.
I have tried writing tests by looking at several tutorials with no luck. I have tried the following tutorials:
Test Custom Hooks Using React Hooks Testing Library
Testing custom react hooks with jest
A Complete Guide to Testing React Hooks
UPDATE: Changed tests, my first test passes (resolve) my second one does not. Based on the third tutorial.
NEW TESTS
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import useFetch from "./useFetch";
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
let container = null;
describe("useFetch tests", () => {
beforeEach(() => {
// setup a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("useFetch returns data on success", async () => {
function fetchMock(url) {
return new Promise((resolve) => setTimeout(() => {
resolve({
status: 200,
ok: true,
text: () => Promise.resolve({
data: "data from api"
})
});
}, 300));
}
jest.spyOn(global, "fetch").mockImplementation(fetchMock);
act(() => {
render(<TestComponent url="url1" />, container);
});
expect(container.textContent).toBe("loading");
await sleep(500);
expect(container.textContent).toBe("data from api");
});
it("useFetch return error on fail", async () => {
// const a = 200 + Math.random() * 300;
// console.log(a)
// let promise = new Promise(function (resolve, reject) {
// // after 1 second signal that the job is finished with an error
// setTimeout(() => reject("error"), a);
// });
// function fetchMock(url) {
// return promise;
// }
function fetchMock(url) {
return new Promise((resolve) => setTimeout(() => {
resolve({
status: 404,
ok: true,
text: () => Promise.resolve({
data: "data from api"
})
});
}, 200 + Math.random() * 300));
}
jest.spyOn(global, "fetch").mockImplementation(fetchMock);
act(() => {
render(<TestComponent url="url1" />, container);
});
expect(container.textContent).toBe("loading");
await sleep(500);
expect(container.textContent).toBe("error");
});
});
function TestComponent({ url }) {
const {
data, response, isLoading, error
} = useFetch(url);
if (isLoading) {
return <div>loading</div>;
}
if (data) {
return <div>{data.data}</div>
}
if (error) {
return <div>error</div>
}
return <div></div>;
}
OLD TESTS
import { useFetch } from "../../../src/utils/custom-hooks/useFetch";
describe("useFetch tests", () => {
beforeEach(() => {
jest.spyOn(window, "fetch");
});
it("200", () => {
window.fetch.mockResolvedValueOnce({
ok: true,
status: 200,
})
const { result, rerender } = renderHook(
(url) => useFetch(url),
{
url: "url1"
}
);
expect(result.current.response).toBe(undefined);
rerender("url2");
expect(result.current.status).toBe(200);
});
});

WebRTC in react-native (hooks), redux - Unhandled Promise Rejections

I'm developing a react-native application, which uses webRTC.
I extremely liked the minimal version I found here (kudos to baconcheese113!) and I decided to refactor it to create my react component.
I have set up a backend (DynamoDB, Appsync) and a redux store that allows me to:
dispatch an action sendCreateUserControlMsg, which down the line calls the Appsync endpoint to create a new ControlUserMsg
subscribe to a ControlUserMsg, set the flag triggerWebrtcData and save webrtcData in the Redux state
The following component (which for now calls itself), sometimes works, but mostly doesn't. I feel that the problem is related to JS Promises, but I do not fully understand how I should design the component to avoid race conditions.
import React, { useState, useEffect } from 'react';
import { View, SafeAreaView, Button, StyleSheet } from 'react-native';
import { RTCPeerConnection, RTCView, mediaDevices } from 'react-native-webrtc';
import { sendCreateUserControlMsg } from '../redux/actions/UserControlMsgActions';
import controlMsgActions from './../model/control_msg_actions';
import webrtcActionTypes from './../model/webrtc_action_types';
import { useDispatch, useSelector } from "react-redux";
import * as triggersMatch from '../redux/actions/TriggersMatchActions';
var IS_LOCAL_USER = true //manual flag I temporarily set
var localUserID = '00';
var localUser = 'localUser'
var remoteUserID = '01';
var remoteUser = 'remoteUser'
if (IS_LOCAL_USER) {
var matchedUserId = remoteUserID
var user_id = localUserID;
var user = localUser
}
else {
var matchedUserId = localUserID
var user_id = remoteUserID;
var user = remoteUser
}
export default function App() {
const dispatch = useDispatch();
var triggersMatchBool = useSelector(state => state.triggers_match)
var webrtcData = useSelector(state => state.webrtc_description.webrtcData)
const [localStream, setLocalStream] = useState();
const [remoteStream, setRemoteStream] = useState();
const [cachedLocalPC, setCachedLocalPC] = useState();
const [cachedRemotePC, setCachedRemotePC] = useState();
const sendICE = (candidate, isLocal) => {
var type
isLocal ? type = webrtcActionTypes["NEW_ICE_CANDIDATE_FROM_LOCAL"] : type = webrtcActionTypes["NEW_ICE_CANDIDATE_FROM_REMOTE"]
var payload = JSON.stringify({
type,
candidate
})
console.log(`Sending ICE to ${matchedUserId}`)
dispatch(sendCreateUserControlMsg(matchedUserId, user_id, user, payload, controlMsgActions["WEBRTC_DATA"]));
}
const sendOffer = (offer) => {
type = webrtcActionTypes["OFFER"]
var payload = JSON.stringify({
type,
offer
})
console.log(`Sending Offer to ${matchedUserId}`)
dispatch(sendCreateUserControlMsg(matchedUserId, user_id, user, payload, controlMsgActions["WEBRTC_DATA"]));
}
const sendAnswer = (answer) => {
type = webrtcActionTypes["ANSWER"]
var payload = JSON.stringify({
type,
answer
})
console.log(`Sending answer to ${matchedUserId}`)
dispatch(sendCreateUserControlMsg(matchedUserId, user_id, user, payload, controlMsgActions["WEBRTC_DATA"]));
}
const [isMuted, setIsMuted] = useState(false);
// START triggers
async function triggerMatchWatcher() {
if (triggersMatchBool.triggerWebrtcData) {
dispatch(triggersMatch.endTriggerWebrtcData());
switch (webrtcData.type) {
case webrtcActionTypes["NEW_ICE_CANDIDATE_FROM_LOCAL"]:
try {
setCachedRemotePC(cachedRemotePC.addIceCandidate(webrtcData.candidate))
} catch (error) {
console.warn('ICE not added')
}
break;
case webrtcActionTypes["NEW_ICE_CANDIDATE_FROM_REMOTE"]:
try {
setCachedLocalPC(cachedLocalPC.addIceCandidate(webrtcData.candidate))
} catch (error) {
console.warn('ICE not added')
}
break;
case webrtcActionTypes["OFFER"]:
console.log('remotePC, setRemoteDescription');
try {
await cachedRemotePC.setRemoteDescription(webrtcData.offer);
console.log('RemotePC, createAnswer');
const answer = await cachedRemotePC.createAnswer();
setCachedRemotePC(cachedRemotePC)
sendAnswer(answer);
} catch (error) {
console.warn(`setRemoteDescription failed ${error}`);
}
case webrtcActionTypes["ANSWER"]:
try {
console.log(`Answer from remotePC: ${webrtcData.answer.sdp}`);
console.log('remotePC, setLocalDescription');
await cachedRemotePC.setLocalDescription(webrtcData.answer);
setCachedRemotePC(cachedRemotePC)
console.log('localPC, setRemoteDescription');
await cachedLocalPC.setRemoteDescription(cachedRemotePC.localDescription);
setCachedLocalPC(cachedLocalPC)
} catch (error) {
console.warn(`setLocalDescription failed ${error}`);
}
}
}
}
useEffect(() => {
triggerMatchWatcher()
}
);
const startLocalStream = async () => {
// isFront will determine if the initial camera should face user or environment
const isFront = true;
const devices = await mediaDevices.enumerateDevices();
const facing = isFront ? 'front' : 'environment';
const videoSourceId = devices.find(device => device.kind === 'videoinput' && device.facing === facing);
const facingMode = isFront ? 'user' : 'environment';
const constraints = {
audio: true,
video: {
mandatory: {
minWidth: 500, // Provide your own width, height and frame rate here
minHeight: 300,
minFrameRate: 30,
},
facingMode,
optional: videoSourceId ? [{ sourceId: videoSourceId }] : [],
},
};
const newStream = await mediaDevices.getUserMedia(constraints);
setLocalStream(newStream);
};
const startCall = async () => {
const configuration = { iceServers: [{ url: 'stun:stun.l.google.com:19302' }] };
const localPC = new RTCPeerConnection(configuration);
const remotePC = new RTCPeerConnection(configuration);
localPC.onicecandidate = e => {
try {
console.log('localPC icecandidate:', e.candidate);
if (e.candidate) {
sendICE(e.candidate, true)
}
} catch (err) {
console.error(`Error adding remotePC iceCandidate: ${err}`);
}
};
remotePC.onicecandidate = e => {
try {
console.log('remotePC icecandidate:', e.candidate);
if (e.candidate) {
sendICE(e.candidate, false)
}
} catch (err) {
console.error(`Error adding localPC iceCandidate: ${err}`);
}
};
remotePC.onaddstream = e => {
console.log('remotePC tracking with ', e);
if (e.stream && remoteStream !== e.stream) {
console.log('RemotePC received the stream', e.stream);
setRemoteStream(e.stream);
}
};
localPC.addStream(localStream);
// Not sure whether onnegotiationneeded is needed
// localPC.onnegotiationneeded = async () => {
// try {
// const offer = await localPC.createOffer();
// console.log('Offer from localPC, setLocalDescription');
// await localPC.setLocalDescription(offer);
// sendOffer(localPC.localDescription)
// } catch (err) {
// console.error(err);
// }
// };
try {
const offer = await localPC.createOffer();
console.log('Offer from localPC, setLocalDescription');
await localPC.setLocalDescription(offer);
sendOffer(localPC.localDescription)
} catch (err) {
console.error(err);
}
setCachedLocalPC(localPC);
setCachedRemotePC(remotePC);
};
const switchCamera = () => {
localStream.getVideoTracks().forEach(track => track._switchCamera());
};
const closeStreams = () => {
if (cachedLocalPC) {
cachedLocalPC.removeStream(localStream);
cachedLocalPC.close();
})
}
if (cachedRemotePC) {
cachedRemotePC.removeStream(localStream);
cachedRemotePC.close();
})
}
setLocalStream();
setRemoteStream();
setCachedRemotePC();
setCachedLocalPC();
};
return (
<SafeAreaView style={styles.container}>
{!localStream && <Button title="Click to start stream" onPress={startLocalStream} />}
{localStream && <Button title="Click to start call" onPress={startCall} disabled={!!remoteStream} />}
{localStream && (
<View style={styles.toggleButtons}>
<Button title="Switch camera" onPress={switchCamera} />
</View>
)}
<View style={styles.rtcview}>
{localStream && <RTCView style={styles.rtc} streamURL={localStream.toURL()} />}
</View>
<View style={styles.rtcview}>
{remoteStream && <RTCView style={styles.rtc} streamURL={remoteStream.toURL()} />}
</View>
<Button title="Click to stop call" onPress={closeStreams} disabled={!remoteStream} />
</SafeAreaView>
);
}
const styles = StyleSheet.create({
// omitted
});
The most common errors I receive are:
Error: Failed to add ICE candidate
Possible Unhandled Promise Rejection
and
setLocalDescription failed TypeError: Cannot read property 'sdp' of
undefined
If I console.log I can see that are JS Promise, but since are not a functions I cannot use .then().
How can I call the addIceCandidate method or setLocalDescription method without incurring in the Unhandled Promise Rejection errors?
What are the best practices to work with WebRTC in react-native?

redux-thunk: actions must be plain objects

I have been trying to use redux and redux-thunk to help get a json file from a api and have been getting a warning stating that action must be a plain object. I am really confused as to where the issue is in the code. i have tried following many other stackoverflow posts and a couple of guides online and have not really got a good grasp of where I am going wrong. I understand that this is a problem with how I am referencing async and dispatch but do not know how to fix it.
This is the function that causes the warning to appear in the simulator
export const fetchData = url => {
console.log("Should enter async dispatch");
return async (dispatch) => {
dispatch(fetchingRequest());
fetch("https://randomuser.me/api/?results=10")
.then(response => {
if (response.ok) {
let json = response.json();
dispatch(fetchingSuccess(json));
console.log("JSON", json);
}
})
.catch(error => {
dispatch(fetchingFailure(error));
console.log("Error", error);
});
};
};
Here is the output in the console
Possible Unhandled Promise Rejection (id: 0):
Error: Actions must be plain objects. Use custom middleware for async actions.
Error: Actions must be plain objects. Use custom middleware for async actions.
Edit: including setup of middleware
I have the middleware setup in the index.js file of my app
index.js
import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";
import { Provider } from "react-redux";
import React, { Components } from "react";
import { createStore, applyMiddleware } from "redux";
import appReducer from "./src/data/redux/reducers/appReducer";
import thunk from "redux-thunk";
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
const store = createStoreWithMiddleware(appReducer);
console.log("Store", store.getState());
const AppContainer = () => (
<Provider store = {store}>
<App />
</Provider>
);
AppRegistry.registerComponent(appName, () => AppContainer);
I learned this implementation of store from a Youtube Tutorial.
Edit 2: Adding in the fetchData call
I call fetchData in a _onPress function like this
_onPress = () => {
const {fetchData} = this.props;
let url = "https://randomuser.me/api/?results=10";
fetchData(url);
console.log("should have fetched");
};
this is how my app has been connected to redux
const mapStateToProps = state => {
return { response: state };
};
const mapStateToDispatch = dispatch => ({
fetchData: url => dispatch(fetchData(url)),
});
export default connect(
mapStateToProps,
mapStateToDispatch
)(SearchScreen);
these are the action in my app
export const fetchingRequest = () => {
{
type: FETCHING_REQUEST;
}
};
export const fetchingSuccess = json => {
{
type: FETCHING_SUCCESS;
payload: json;
}
};
export const fetchingFailure = error => {
{
type: FETCHING_FAILURE;
payload: error;
}
};
I was able to figure out the problem thanks to working through the steps in the comments thanks to Michael Cheng. I ended up finding that the problem was that i had actions with plain objects but they were not returning anything.
The original actions were
export const fetchingRequest = () => {
{
type: FETCHING_REQUEST;
}
};
export const fetchingSuccess = json => {
{
type: FETCHING_SUCCESS;
payload: json;
}
};
export const fetchingFailure = error => {
{
type: FETCHING_FAILURE;
payload: error;
}
};
to this
export const fetchingRequest = () => {
return {
type: FETCHING_REQUEST
}
};
export const fetchingSuccess = json => {
return {
type: FETCHING_SUCCESS,
payload: json
}
};
export const fetchingFailure = error => {
return {
type: FETCHING_FAILURE,
payload: error
};
};
with including the return for each action