I am using Agora for the first time and am facing an issue while adding remote streams. When 1st user joins a channel it correctly show it's stream but when another user joins the same channel, the stream of the second user appears on the first user's screen but the stream of first user does not appear on the second user stream. i.e. a user is only able to see the streams of the people joined after it and not of those that joined before it. Below is the code.
import React, { useEffect, useState, useRef } from "react";
import ReactDOM from "react-dom";
import "./App.css";
import { options, rtc } from "./constants";
import AgoraRTC from "agora-rtc-sdk-ng";
function App() {
const RemoteUser = [];
async function handleSubmit(e) {
try {
if (channelRef.current.value === "") {
return console.log("Please Enter Channel Name");
}
setJoined(true);
rtc.client = AgoraRTC.createClient({ mode: "rtc", codec: "h264" });
const uid = await rtc.client.join(
options.appId,
channelRef.current.value,
options.token,
null
);
// Create an audio track from the audio captured by a microphone
rtc.localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack();
// Create a video track from the video captured by a camera
rtc.localVideoTrack = await AgoraRTC.createCameraVideoTrack();
rtc.localVideoTrack.play("local-stream");
rtc.client.on("user-published", async (user, mediaType) => {
// Subscribe to a remote user
await rtc.client.subscribe(user);
console.log("subscribe success");
// console.log(user);
if (mediaType === "video" || mediaType === "all") {
// Get `RemoteVideoTrack` in the `user` object.
const remoteVideoTrack = user.videoTrack;
console.log(remoteVideoTrack);
// Dynamically create a container in the form of a DIV element for playing the remote video track.
const PlayerContainer = React.createElement("div", {
id: user.uid,
className: "stream",
});
ReactDOM.render(
PlayerContainer,
document.getElementById("remote-stream")
);
user.videoTrack.play(`${user.uid}`);
}
if (mediaType === "audio" || mediaType === "all") {
// Get `RemoteAudioTrack` in the `user` object.
const remoteAudioTrack = user.audioTrack;
// Play the audio track. Do not need to pass any DOM element
remoteAudioTrack.play();
}
});
rtc.client.on("user-unpublished", (user) => {
// Get the dynamically created DIV container
const playerContainer = document.getElementById(user.uid);
console.log(playerContainer);
// Destroy the container
playerContainer.remove();
});
// Publish the local audio and video tracks to the channel
await rtc.client.publish([rtc.localAudioTrack, rtc.localVideoTrack]);
console.log("publish success!");
} catch (error) {
console.error(error);
}
}
async function handleLeave() {
try {
const localContainer = document.getElementById("local-stream");
rtc.localAudioTrack.close();
rtc.localVideoTrack.close();
setJoined(false);
localContainer.textContent = "";
// Traverse all remote users
rtc.client.remoteUsers.forEach((user) => {
// Destroy the dynamically created DIV container
const playerContainer = document.getElementById(user.uid);
playerContainer && playerContainer.remove();
});
// Leave the channel
await rtc.client.leave();
} catch (err) {
console.error(err);
}
}
const [joined, setJoined] = useState(false);
const channelRef = useRef("");
const remoteRef = useRef("");
const leaveRef = useRef("");
return (
<>
<div className="container">
<input
type="text"
ref={channelRef}
id="channel"
placeholder="Enter Channel name"
/>
<input
type="submit"
value="Join"
onClick={handleSubmit}
disabled={joined ? true : false}
/>
<input
type="button"
ref={leaveRef}
value="Leave"
onClick={handleLeave}
disabled={joined ? false : true}
/>
</div>
{joined ? (
<>
<div id="local-stream" className="stream local-stream"></div>
<div
id="remote-stream"
ref={remoteRef}
className="stream remote-stream"
></div>
</>
) : null}
</>
);
}
export default App;
create rtcClient outside the function handleSubmit.
Also register the events when document/Page is ready.
rtc.client = AgoraRTC.createClient({ mode: "rtc", codec: "h264" });
rtc.client.on("user-published", async (user, mediaType) => {
//Your code
}
rtc.client.on("user-unpublished", (user) => {
//Your code
}
It will show the users list accurately.
Related
I'm developing a Video chat app with React, Redux and Zoom Web SDK.
What I'm trying to do is create camera (canvas API) for each user on room enter, then removing on room leave.
I implemented the feature above, but every time new participants enter or current participants leave, all cameras reset and turn off.
I think this has something to do with states, and rerendering.
Here's my core code snippet of the feature.
const Session = () => {
... otherStates
const stream = useRef({})
const [ sessionUsers, setSessionUsers ] = useState([]) // This is the state for currentParticipants.
useEffect(() => {
... otherListeners.
client.on('peer-video-state-change',async(payload) => {
const { action, userId } = payload
try {
await participantsRender(action,userId)
} catch (error) {
console.log(error)
}
})
client.on('user-added', (user) => {
const participants = client.getAllUser();
setSessionUsers(participants)
})
client.on('user-removed', (user) => {
const participants = client.getAllUser();
setSessionUsers(participants)
})
},[client])
const participantsRender = async(action,userId) => {
if (!action || !userId) return console.log('Empty Param')
const canvas = document.querySelector(`.canvas[data-canvas_id="${userId}"]`)
if (action === 'Start'){
await stream.current.renderVideo(canvas, userId, 640, 360, 0, 0, 2);
} else if (action === 'Stop') {
await stream.current.stopRenderVideo(canvas, userId);
} else {
console.log('error')
}
}
const toggleVideo = async() => { // This is a feature to start/stop video for the user.
if (!stream) return
const uid = client.getCurrentUserInfo().userId
const canvas = document.querySelector(`.canvas[data-canvas_id="${uid}"]`)
if(!stream.current?.isCapturingVideo()) {
try {
await stream.current?.startVideo();
stream.current?.renderVideo(canvas, uid, 640, 360, 0, 0, 2);
} catch (error) {
console.log(error)
}
} else if (stream.current.isCapturingVideo()){
try {
await stream.current.stopVideo()
stream.current?.stopRenderVideo(canvas, uid)
} catch (error) {
console.log(error)
}
}
}
const CanvasRenderer = React.memo(( { state } ) => {
if (!state) return <></>
return state.map((item,index) => (
<div className = "canvas_container" key = {item.userId}>
<span className = "canvas_name_container">
<p className = "canvas_name_self">{item.userId || ''}</p>
</span>
<canvas className = "canvas" data-canvas_id = {item.userId}></canvas>
</div>
))
})
return (
<div>
<div className = "canvas_list_container">
<CanvasRenderer state = {sessionUsers} /> // This is the renderer for cameras.
</div>
</div>
)
}
It seems that you are using multiple canvases for video rendering. This is not recommended. If you need to manage multiple participants in a session, we suggest that you can maintain a participant list, use a single canvas, and programmatically decide which should draw on the canvas. If you are familiar with React, you can refer to the react-sample-app.
I'm facing this issue where the cookie value will be set as undefined for some reason during the render in profile page. I have checked on this error for hours and I can confirm that there are no external values affecting the cookie data, other than during the login, edit profile, and logout process.
For reference, these are the codes that occurred in affecting cookies:
import axios from 'axios';
import { Link, useHistory } from 'react-router-dom';
import { useState, useEffect, useRef } from "react";
import { useSnackbar } from 'notistack';
import Cookies from 'js-cookie'
// Functions
import { validateJSON } from '../functions/validateJSON'
import { destroyCookies } from '../functions/destroyCookies'
// Material UI
import { TextField } from "#material-ui/core";
const LoginForm = ({setLogin, login}) => {
const history = useHistory();
const [errors, setErrors] = useState({});
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
// checking the first initial render
const firstRender = useRef(false);
// Defining snackbar from MaterialUI
const { enqueueSnackbar } = useSnackbar();
// creating error message object literals for form validation
const validation = () => {
let validate = {}
validate.username = username !== "" | null | " " ? "" : "* This field is required";
validate.password = password !== "" | null | " " ? "" : "* This field is required";
setErrors ({
// Saving errors into the state object via seperator operator
...validate
})
// return the object of values, by using every() to validate the array with the provided function
// if each of the values from the input delivered in validate object is valid according to the condition set within every(), then return boolean.
return Object.values(validate).every(x => x == "") // if the object value is not empty then it will return true
}
const handleSubmit = (e) => {
e.preventDefault(); // preventing the form onsubmit automatically refresh the page
// check if the forms are validated or not
if (validation()) {
// if the forms have no error then proceed with request
// Send POST request to server for obtaining the related data
let formData = new FormData();
formData.append("username", username);
formData.append("password", password);
const API_PATH = `http://localhost:80/APU/FYP/estiatorio/src/api/login.php`; // direct it to the PHP folder
axios.post(API_PATH, formData) // asynchronous, therefore promises
.then((res) => {
// if POST is a success then output a snackbar from MaterialUI
if (res.data == false) {
// if user data not found
enqueueSnackbar('Username or password is wrong, please try again.', {variant: 'error'});
} else if (validateJSON(res.data) == true) {
// if data is in JSON format, means the user data retrieved
// proceed to extract the data and set it into React Session
console.log(res.data);
Promise.allSettled(res.data.map(async(data) => {
Cookies.set("user_id", data.user_id);
Cookies.set("profile_image", data.profile_image);
Cookies.set("username", data.username);
Cookies.set("password", data.password);
Cookies.set("email", data.email);
Cookies.set("first_name", data.first_name);
Cookies.set("last_name", data.last_name);
Cookies.set("phone_number", data.phone_number);
Cookies.set("dob", data.dob);
Cookies.set("role", data.role);
Cookies.set("status", data.status);
Cookies.set("registered_date", data.registered_date);
Cookies.set("gender", data.gender);
// check if the user_id has been defined within the session, then login is a success
if (Cookies.get("user_id")) {
enqueueSnackbar("Logged in", {variant: 'success'});
// redirect the user to their specified destination based on their role
if (Cookies.get("role") == "0") {
history.push('/dashboard');
} else if (Cookies.get("role") == "1") {
history.push('/profile');
} else {
history.push('/dashboard');
}
} else {
destroyCookies();
enqueueSnackbar("Error: Login failed, please try again.", {variant: 'error'});
}
}));
} else {
// in this case res.data must be returning an error message
enqueueSnackbar(res.data, {variant: 'error'});
}
}).then(() => {
setLogin(login => login + 1);
})
.catch((err) => {
enqueueSnackbar(err, {variant: 'error'});
throw Error("Error: Login process failed for some reason. Error: " + err); // making a custom error message that will show in console
});
} else {
// error message
enqueueSnackbar('Error: Form is invalid, please try again', {variant: 'error'});
}
}
useEffect(() => {
// Perform input validation after setState(), by using custom hook and useRef
if (firstRender.current) {
validation();
} else {
firstRender.current = true;
}
}, [username, password])
useEffect(() => {
// Page validation on login and registration page
if (Cookies.get("user_id")) {
// if the user already logged in, check whether the account is active or not
if (Cookies.get("status") == 0) {
// if it is, proceed to redirect back to their page accordingly based on their role
// 0: Admin = Dashboard; 1: Customer = Profile Page; 2: Staff = Dashboard
enqueueSnackbar("Notice: You have already logged in", {variant: 'info'});
if (Cookies.get("role") == 0) {
history.push('/dashboard');
} else if (Cookies.get("role") == 1) {
history.push('/profile');
} else {
history.push('/dashboard');
}
} else {
enqueueSnackbar('Your account is inactive, you are unauthorized to access this page.', {variant: 'error'});
history.push('/');
// immediately break session
destroyCookies();
}
}
}, [])
return (
<div className="login-form">
<form onSubmit={handleSubmit}>
<h1>Sign In</h1>
<TextField
{...(errors.username && {error: true, helperText: errors.username})}
id="standard-required"
label="Username"
type="text"
fullWidth
required
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<TextField
{...(errors.password && {error: true, helperText: errors.password})}
id="standard-password-input"
label="Password"
type="password"
fullWidth
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<div className="reset-pw-link">
<Link to="/reset-password">Forgot Password</Link>
</div>
<input type="submit" className="elementor-button-link elementor-button elementor-size-sm" value="login" />
</form>
</div>
);
}
export default LoginForm;
Nevermind, found the cause out of it. ITS THE LITTLE COOKIES.SET() ON ROLE SOMEHOW SNEAKED INTO NAVBAR AND TURN IT INTO UNDEFINED AS ITS INSIDE THE CONDITIONAL STATEMENT. OMG
I made a search bar that allows the user to search all sports available in one specific city (if sport is not defined) or a specific sport in a specific city (if sport is defined).
City will allways be defined.
I put 2 inputs (city and sport) on my searchbar and I want immediate results (so that there is a request to my API without any button "search" that triggers the request).
So when the user types something on the city input it triggers a request to the API and when he types something on the sport input it retriggers the request but this time with the city and the sport defined.
Both inputs values are store in states (city and sport).
I manage to do something that seems to work, the only problem is that if I types a sport in my input search, it does not update my request to the API. I have to retype the sport in my input a second time so that the request is updated.
I don't know why it does not update the first time I types something in my sport input because I have specified on my useEffect array that it must re render when the sport state changes.
Can someone help me understand this ?
My code :
import React, { useState, useEffect } from "react";
import style from "../styles/pdrs.module.css";
import axios from "axios";
import SearchBar from "../components/SearchBar";
const Pdrs = ({ setSearchCity, searchSport, setSearchSport }) => {
// if request's result is loading
const [isLoading, setIsLoading] = useState(false);
// search result
const [searchresults, setSearchresults] = useState(
"Lancez une recherche avec au moins une ville !"
);
// state for the searchbar request
const [city, setCity] = useState("");
const [sport, setSport] = useState(0);
// get city's id for API's request
const fetchCity = async () => {
setIsLoading(true);
try {
// city search
const cityResponse = await axios.get(
`${baseAPI}/city/name=${searchCity}`
);
const city = cityResponse.data;
setCity(city);
setIsLoading(false);
} catch (error) {
console.log(error.message);
setIsLoading(false);
}
};
//fetching sport id
const fetchSport = async () => {
setIsLoading(true);
try {
const sportResponse = await axios.get(
`${baseAPI}/activity/name=${searchSport}`
);
setSport(sportResponse.data.data[0].macro_activity_id);
setIsLoading(false);
} catch (error) {
console.log(error.message);
}
};
//fetching final request response
const fetchDataRequest = async () => {
try {
setIsLoading(true);
const results = await axios.get(
`${baseAPI}/pdrs?city_id=${city.id}${
sport ? "¯o_activity_id=" + sport : ""
}`
);
// manage search results
if (results.data.nb_results === 1) {
setSearchresults({
data: [results.data.data],
nb_results: 1,
});
setNbResults(1);
setIsLoading(false);
} else {
setSearchresults(results.data);
setNbResults(results.data.nb_results);
setIsLoading(false);
}
} catch (error) {
console.log(error.message);
setSearchresults(
"Sorry, nothing was found... !"
);
}
};
useEffect(() => {
if (searchCity) {
fetchCity();
}
if (searchSport) {
fetchSport();
}
}, [searchCity, searchSport]);
useEffect(() => {
if (searchCity) {
fetchDataRequest();
}
}, [searchCity, searchSport]);
console.log(searchresults);
return <>
<main className={`container ${style.pdrs}`}>
<section className={style.searchbar}>
<SearchBar
searchCity={searchCity}
setSearchCity={setSearchCity}
searchSport={searchSport}
setSearchSport={setSearchSport}
searchInstallation={searchInstallation}
setSearchInstallation={setSearchInstallation}
searchType={searchType}
setSearchType={setSearchType}
setPage={setPage}
/>
</section>
<section className={style.results}>
{isLoading ? (
<div>Loading...</div>
) : typeof searchresults === "string" ? (
<div className={`${style.container} ${style.noResults}`}>
<h2>{searchresults}</h2>
</div>
) : (
<>
<div className={style.container}>
<div className={style.resultsList}>
{searchresults.data.map((pdrs) => {
return (
// some code displaying the result
);
})}
</div>
</div>
</>
)}
</section>
</main>
</>;
};
export default Pdrs;
Since you are having two useEffect and one is setting city and sport you would need to make debounce for making a call for fetching list by itself.
I would suggest that you firstly make changes to your use effect for API call fetchDataRequest:
useEffect(() => {
if (searchCity) {
fetchDataRequest();
}
}, [city, sport]);
You would listen to the actual data from BE, not from input that you fill.
And secondly you can use library useDebounce from here https://www.npmjs.com/package/use-debounce and use useDebounceCallback to delay calling API call after you select sport/city.
So i've basically got 2 components on my page.
First is the search component where the users need to type their username and second one where their stats get displayed
and here is my API request call in App.js
useEffect(()=>{
const fetchStats = async ()=> {
const result = await axios.get(`https://cors-anywhere.herokuapp.com/https://public-api.tracker.gg/v2/csgo/standard/profile/steam/${username}`,
{
headers: {
'TRN-Api-Key' : '***************************',
}
}
)
if(username !== null){
console.log(result.data)
setStats(result.data)
}
}
fetchStats()
},[username])
and this is the search component
const Search = ({setInputText, setUsername, inputText, username}) => {
const inputHandler = (e)=> {
setInputText(e.target.value)
}
const searchHandler = (e)=> {
e.preventDefault()
setUsername(inputText)
}
return (
<div>
<form>
<input value={inputText} onChange={inputHandler} type="text"/>
<button onClick={searchHandler}>Search</button>
</form>
</div>
)
}
What i'm having an issue with is when i click a button in the username component the value(username) from the form gets stored in the 'username' state in App.js. Now i'm using this code in the stats component.
const Stats = ({stats}) => {
return (
<div>
<h1>{stats.data.platformInfo.platformUserHandle}</h1>
</div>
)
}
export default Stats
Here stats.data.platformInfo.platformUserHandle doesn't exist when the app starts so it gives me a LOT of errors. How do i keep the app from crashing till the user has input something and data can be sent to the stats component?
I'm working on a raise invoice page, in which user can raise a invoice on clicking of a button, I would call a api call and after getting the response I want to send some data to a page(RaisedInvoice.jsx) which should open in a new tab, how can i do it. The thing which I am not getting is how to open a page in new tab on click of a button in ReactJs.
RaiseInvoice.jsx:
import React from 'react';
import Links from './Links.jsx';
import history from './history.jsx';
import axios from 'axios';
class RaiseInvoice extends React.Component {
constructor(props) {
super(props);
// This binding is necessary to make `this` work in the callback
this.state = {projects: [], searchParam : ''};
this.raiseInvoiceClicked = this.raiseInvoiceClicked.bind(this);
}
raiseInvoiceClicked(){
// here i wish to write the code for opening the page in new tab.
}
render() {
return (
<div>
<Links activeTabName="tab2"></Links>
<div className="container">
<div className = "row col-md-4">
<h1>Raise Invoice...</h1>
</div>
<div className = "row col-md-4"></div>
<div className = "row col-md-4" style ={{"marginTop":"24px"}}>
<button type="button" className="btn btn-default pull-right" onClick={this.raiseInvoiceClicked}>Raise Invoice</button>
</div>
</div>
</div>
)
}
}
export default RaiseInvoice;
Since you were going to send big data, appending them to your target URL looks shabby. I would suggest you use 'LocalStorage' for this purpose. So your code looks like this,
raiseInvoiceClicked(){
// your axios call here
localStorage.setItem("pageData", "Data Retrieved from axios request")
// route to new page by changing window.location
window.open(newPageUrl, "_blank") //to open new page
}
In your RaisedInvoice.jsx, retrieve the data from Local Storage like this,
componentWillMount() {
localStorage.pagedata= "your Data";
// set the data in state and use it through the component
localStorage.removeItem("pagedata");
// removing the data from localStorage. Since if user clicks for another invoice it overrides this data
}
You can just use plain JS to do it and append some query perimeters with it
raiseInvoiceClicked(){
const url = 'somesite.com?data=yourDataToSend';
window.open(url, '_blank');
}
Instead of calling raiseInvoiceClicked() function inside onclick method, you can try
onClick="window.open('your_url')"
in your code.
Simply do this!
const openLinkInNewTab = ( url ) => {
const newTab = window.open(url, '_blank', 'noopener,noreferrer');
if ( newTab ) newTab.opener = null;
}
//...
return (
//...
<button onClick={ () => openLinkInNewTab('your.url')}> Click Here </button>
//...
)
You can open it in a new window using the following code.
Please note that for props you can pass any child components that should be rendered inside new window.
const RenderInWindow = (props) => {
const [container, setContainer] = useState(null);
const newWindow = useRef(null);
useEffect(() => {
// Create container element on client-side
setContainer(document.createElement("div"));
}, []);
useEffect(() => {
// When container is ready
if (container) {
// Create window
newWindow.current = window.open(
"",
"",
"width=600,height=400,left=200,top=200"
);
// Append container
newWindow.current.document.body.appendChild(container);
// Save reference to window for cleanup
const curWindow = newWindow.current;
// Return cleanup function
return () => curWindow.close();
}
}, [container]);
return container && createPortal(props.children, container);
};
Pass this data with props:
let href = '...url_to_redirect...'; let data_to_send = [...];
let api_href = '...url_api_where_sent_data.../?data_to_send';
export const DictDefaultOptions = (url=(api_href), method='GET') => {
let defaultOptions = {
url : url,
method : method,
mode : 'cors',
headers : {'Access-Control-Allow-Origin':'*'}
};
return defaultOptions };
let sentData = {
method: defaultOptions.method,
mode: defaultOptions.mode
};
send_data_to_api = () => {
let api_return = await fetch(api_href, sentData)
.then(response => response.json())
.then(responseText => {
data = (JSON.parse(JSON.stringify(responseText)))
})
.catch(error => {
console.log(`${requestURL} error: `, error)
});
do { await this.wait(100) } while(Object.keys(api_return).length <= 0);
if (Object.keys(api_return).length > 0) {
return window.open(href, "_blank")
}
};