Related
I have two files contact.js and functions.js. I am using firestore realtime functionality.
Here is my functions.js file code:
export const getUserContacts = () => {
const contactDetailsArr = [];
return db.collection("users").doc(userId)
.onSnapshot(docs => {
const contactsObject = docs.data().contacts;
for (let contact in contactsObject) {
db.collection("users").doc(contact).get()
.then(userDetail => {
contactDetailsArr.push({
userId: contact,
lastMessage: contactsObject[contact].lastMsg,
time: contactsObject[contact].lastMsgTime,
userName:userDetail.data().userName,
email: userDetail.data().emailId,
active: userDetail.data().active,
img: userDetail.data().imageUrl,
unreadMsg:contactsObject[contact].unreadMsg
})
})
}
console.log(contactDetailsArr);
return contactDetailsArr;
})
}
in contact.js when I do:
useEffect(() => {
let temp = getUserContacts();
console.log(temp);
}, [])
I want to extract data of contactDetailsArr in contacts.js but I get the value of temp consoled as:
ƒ () {
i.Zl(), r.cs.ws(function () {
return Pr(r.q_, o);
});
}
How do I extract the array data in my case?
The onSnapshot() returns a function that can be used to detach the Firestore listener. When using a listener, it's best to set the data directly into state rather than returning something from that function. Try refactoring the code as shown below:
const [contacts, setContacts] = useState([]);
useEffect(() => {
const getUserContacts = () => {
const contactDetailsArr = [];
const detach = db.collection("users").doc(userId)
.onSnapshot(docs => {
const contactsObject = docs.data().contacts;
const contactsSnap = await Promise.all(contactsObject.map((c) => db.collection("users").doc(c).get()))
const contactDetails = contactsSnap.map((d) => ({
id: d.id,
...d.data()
// other fields like unreadMsg, time
}))
// Update in state
setContacts(contactDetails);
})
}
getUserContacts();
}, [])
Then use contacts array to map data in to UI directly.
Assumptions
This answer assumes a user's data looks like this in your Firestore:
// Document at /users/someUserId
{
"active": true,
"contacts": {
"someOtherUserId": {
"lastMsg": "This is a message",
"lastMsgTime": /* Timestamp */,
"unreadMsg": true // unclear if this is a boolean or a count of messages
},
"anotherUserId": {
"lastMsg": "Hi some user! How are you?",
"lastMsgTime": /* Timestamp */,
"unreadMsg": false
}
},
"emailId": "someuser#example.com",
"imageUrl": "https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg",
"userName": "Some User"
}
Note: When asking questions in the future, please add examples of your data structure similar to the above
Attaching Listeners with Current Structure
The structure as shown above has a number of flaws. The "contacts" object in the user's data should be moved to a sub-collection of the user's main document. The reasons for this include:
Any user can read another user's (latest) messages (which can't be blocked with security rules)
Any user can read another user's contacts list (which can't be blocked with security rules)
As an individual user messages more users, their user data will grow rapidly in size
Each time you want to read a user's data, you have to download their entire message map even if not using it
As you fill out a user's contacts array, you are fetching their entire user data document even though you only need their active, email, imageUrl, and userName properties
Higher chance of encountering document write conflicts when two users are editing the contact list of the same user (such as when sending a message)
Hard to (efficiently) detect changes to a user's contact list (e.g. new addition, deletion)
Hard to (efficiently) listen to changes to another user's active status, email, profile image and display name as the listeners would be fired for every message update they receive
To fetch a user's contacts once in your functions.js library, you would use:
// Utility function: Used to hydrate an entry in a user's "contacts" map
const getContactFromContactMapEntry = (db, [contactId, msgInfo]) => {
return db.collection("users")
.doc(contactId)
.get()
.then((contactDocSnapshot) => {
const { lastMsg, lastMsgTime, unreadMsg, userName } = msgInfo;
const baseContactData = {
lastMessage: lastMsg,
time: lastMsgTime,
unreadMsg,
userId: contactId
}
if (!contactDocSnapshot.exists) {
// TODO: Decide how to handle unknown/deleted users
return {
...baseContactData,
active: false, // deleted users are inactive, nor do they
email: null, // have an email, image or display name
img: null,
userName: "Deleted user"
};
}
const { active, emailId, imageUrl, userName } = contactDocSnapshot.data();
return {
...baseContactData,
active,
email: emailId,
img: imageUrl,
userName
};
});
};
export const getUserContacts = (db, userId) => { // <-- note that db and userId are passed in
return db.collection("users")
.doc(userId)
.get()
.then((userDataSnapshot) => {
const contactsMetadataMap = userDataSnapshot.get("contacts");
return Promise.all( // <-- waits for each Promise to complete
Object.entries(contactsMetadataMap) // <-- used to get an array of id-value pairs that we can iterate over
.map(getContactFromContactMapEntry.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
);
});
}
Example Usage:
getUserContacts(db, userId)
.then((contacts) => console.log("Contacts data:", contacts))
.catch((err) => console.error("Failed to get contacts:", err))
// OR
try {
const contacts = await getUserContacts(db, userId);
console.log("Contacts data:", contacts);
} catch (err) {
console.error("Failed to get contacts:", err)
}
To fetch a user's contacts, and keep the list updated, using a function in your functions.js library, you would use:
// reuse getContactFromContactMapEntry as above
export const useUserContacts = (db, userId) => {
if (!db) throw new TypeError("Parameter 'db' is required");
const [userContactsData, setUserContactsData] = useState({ loading: true, contacts: [], error: null });
useEffect(() => {
// no user signed in?
if (!userId) {
setUserContactsData({ loading: false, contacts: [], error: "No user signed in" });
return;
}
// update loading status (as needed)
if (!userContactsData.loading) {
setUserContactsData({ loading: true, contacts: [], error: null });
}
let detached = false;
const detachListener = db.collection("users")
.doc(userId)
.onSnapshot({
next: (userDataSnapshot) => {
const contactsMetadataMap = userDataSnapshot.get("contacts");
const hydrateContactsPromise = Promise.all( // <-- waits for each Promise to complete
Object.entries(contactsMetadataMap) // <-- used to get an array of id-value pairs that we can iterate over
.map(getContactFromContactMapEntry.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
);
hydrateContactsPromise
.then((contacts) => {
if (detached) return; // detached already, do nothing.
setUserContactsData({ loading: false, contacts, error: null });
})
.catch((err) => {
if (detached) return; // detached already, do nothing.
setUserContactsData({ loading: false, contacts: [], error: err });
});
},
error: (err) => {
setUserContactsData({ loading: false, contacts: [], error: err });
}
});
return () => {
detached = true;
detachListener();
}
}, [db, userId])
}
Note: The above code will not (due to complexity):
react to changes in another user's active status, email or profile image
properly handle when the setUserContactsData method is called out of order due to network issues
handle when db instance is changed on every render
Example Usage:
const { loading, contacts, error } = useUserContacts(db, userId);
Attaching Listeners with Sub-collection Structure
To restructure your data for efficiency, your structure would be updated to the following:
// Document at /users/someUserId
{
"active": true,
"emailId": "someuser#example.com",
"imageUrl": "https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg",
"userName": "Some User"
}
// Document at /users/someUserId/contacts/someOtherUserId
{
"lastMsg": "This is a message",
"lastMsgTime": /* Timestamp */,
"unreadMsg": true // unclear if this is a boolean or a count of messages
}
// Document at /users/someUserId/contacts/anotherUserId
{
"lastMsg": "Hi some user! How are you?",
"lastMsgTime": /* Timestamp */,
"unreadMsg": false
}
Using the above structure provides the following benefits:
Significantly better network performance when hydrating the contacts list
Security rules can be used to ensure users can't read each others contacts lists
Security rules can be used to ensure a message stays private between the two users
Listening to another user's profile updates can be done without reading or being notified of any changes to their other private messages
You can partially fetch a user's message inbox rather than the whole list
The contacts list is easy to update as two users updating the same contact entry is unlikely
Easy to detect when a user's contact entry has been added, deleted or modified (such as receiving a new message or marking a message read)
To fetch a user's contacts once in your functions.js library, you would use:
// Utility function: Merges the data from an entry in a user's "contacts" collection with that user's data
const mergeContactEntrySnapshotWithUserSnapshot = (contactEntryDocSnapshot, contactDocSnapshot) => {
const { lastMsg, lastMsgTime, unreadMsg } = contactEntryDocSnapshot.data();
const baseContactData = {
lastMessage: lastMsg,
time: lastMsgTime,
unreadMsg,
userId: contactEntryDocSnapshot.id
}
if (!contactDocSnapshot.exists) {
// TODO: Handle unknown/deleted users
return {
...baseContactData,
active: false, // deleted users are inactive, nor do they
email: null, // have an email, image or display name
img: null,
userName: "Deleted user"
};
}
const { active, emailId, imageUrl, userName } = contactDocSnapshot.data();
return {
...baseContactData,
active,
email: emailId,
img: imageUrl,
userName
};
}
// Utility function: Used to hydrate an entry in a user's "contacts" collection
const getContactFromContactsEntrySnapshot = (db, contactEntryDocSnapshot) => {
return db.collection("users")
.doc(contactEntry.userId)
.get()
.then((contactDocSnapshot) => mergeContactEntrySnapshotWithUserSnapshot(contactEntryDocSnapshot, contactDocSnapshot));
};
export const getUserContacts = (db, userId) => { // <-- note that db and userId are passed in
return db.collection("users")
.doc(userId)
.collection("contacts")
.get()
.then((userContactsQuerySnapshot) => {
return Promise.all( // <-- waits for each Promise to complete
userContactsQuerySnapshot.docs // <-- used to get an array of entry snapshots that we can iterate over
.map(getContactFromContactsEntrySnapshot.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
);
});
}
Example Usage:
getUserContacts(db, userId)
.then((contacts) => console.log("Contacts data:", contacts))
.catch((err) => console.error("Failed to get contacts:", err))
// OR
try {
const contacts = await getUserContacts(db, userId);
console.log("Contacts data:", contacts);
} catch (err) {
console.error("Failed to get contacts:", err)
}
To fetch a user's contacts in a way where it's kept up to date, we first need to introduce a couple of utility useEffect wrappers (there are libraries for more robust implementations):
export const useFirestoreDocument = ({ db, path }) => {
if (!db) throw new TypeError("Property 'db' is required");
const [documentInfo, setDocumentInfo] = useState({ loading: true, snapshot: null, error: null });
useEffect(() => {
if (!path) {
setDocumentInfo({ loading: false, snapshot: null, error: "Invalid path" });
return;
}
// update loading status (as needed)
if (!documentInfo.loading) {
setDocumentInfo({ loading: true, snapshot: null, error: null });
}
return db.doc(path)
.onSnapshot({
next: (docSnapshot) => {
setDocumentInfo({ loading: false, snapshot, error: null });
},
error: (err) => {
setDocumentInfo({ loading: false, snapshot: null, error: err });
}
});
}, [db, path]);
return documentInfo;
}
export const useFirestoreCollection = ({ db, path }) => {
if (!db) throw new TypeError("Property 'db' is required");
const [collectionInfo, setCollectionInfo] = useState({ loading: true, docs: null, error: null });
useEffect(() => {
if (!path) {
setCollectionInfo({ loading: false, docs: null, error: "Invalid path" });
return;
}
// update loading status (as needed)
if (!collectionInfo.loading) {
setCollectionInfo({ loading: true, docs: null, error: null });
}
return db.collection(path)
.onSnapshot({
next: (querySnapshot) => {
setCollectionInfo({ loading: false, docs: querySnapshot.docs, error: null });
},
error: (err) => {
setCollectionInfo({ loading: false, docs: null, error: err });
}
});
}, [db, path]);
return collectionInfo;
}
To use that method to hydrate a contact, you would call it from a ContactEntry component:
// mergeContactEntrySnapshotWithUserSnapshot is the same as above
const ContactEntry = ({ db, userId, key: contactId }) => {
if (!db) throw new TypeError("Property 'db' is required");
if (!userId) throw new TypeError("Property 'userId' is required");
if (!contactId) throw new TypeError("Property 'key' (the contact's user ID) is required");
const contactEntryInfo = useFirestoreDocument(db, `/users/${userId}/contacts/${contactId}`);
const contactUserInfo = useFirestoreDocument(db, `/users/${contactId}`);
if ((contactEntryInfo.loading && !contactEntryInfo.error) && (contactUserInfo.loading && !contactUserInfo.error)) {
return (<div>Loading...</div>);
}
const error = contactEntryInfo.error || contactUserInfo.error;
if (error) {
return (<div>Contact unavailable: {error.message}</div>);
}
const contact = mergeContactEntrySnapshotWithUserSnapshot(contactEntryInfo.snapshot, contactUserInfo.snapshot);
return (<!-- contact content here -->);
}
Those ContactEntry components would be populated from a Contacts component:
const Contacts = ({db}) => {
if (!db) throw new TypeError("Property 'db' is required");
const { user } = useFirebaseAuth();
const contactsCollectionInfo = useFirestoreCollection(db, user ? `/users/${user.uid}/contacts` : null);
if (!user) {
return (<div>Not signed in!</div>);
}
if (contactsCollectionInfo.loading) {
return (<div>Loading contacts...</div>);
}
if (contactsCollectionInfo.error) {
return (<div>Contacts list unavailable: {contactsCollectionInfo.error.message}</div>);
}
const contactEntrySnapshots = contactsCollectionInfo.docs;
return (
<>{
contactEntrySnapshots.map(snapshot => {
return (<ContactEntry {...{ db, key: snapshot.id, userId: user.uid }} />);
})
}</>
);
}
Example Usage:
const db = firebase.firestore();
return (<Contacts db={db} />);
Your code seems to be not written with async/await or promise like style
e.g. contactDetailsArr will be returned as empty array
also onSnapshot creates long term subscription to Firestore collection and could be replaced with simple get()
See example on firestore https://firebase.google.com/docs/firestore/query-data/get-data#web-version-9_1
I'm implementing WebRTC Perfect Negotiation in my Vue 2 application. The app will have multiple viewers and a single streamer.
After a lot of logging and debugging, I've resolved some of the problems that I was having. I removed the TURN server in the iceServers configuration, and that allowed the ICE Candidate gathering to finish. Previously it was stuck at "gathering". Now, the two peers have exchanged local/remote descriptions and added ICE candidates, but there still is not a change in the connectionState.
Here is my RTCPeerConnection object:
RTCPeerConnection
canTrickleIceCandidates: true
connectionState: "new"
currentLocalDescription: RTCSessionDescription {type: 'offer', sdp: 'v=0\r\no=- 4764627134364341061 2 IN IP4 127.0.0.1\r\ns…4754 label:f12fee59-268c-4bc3-88c1-8ac27aec8a9c\r\n'}
currentRemoteDescription: RTCSessionDescription {type: 'answer', sdp: 'v=0\r\no=- 3069477756847576830 2 IN IP4 127.0.0.1\r\ns…Nd1pO\r\na=ssrc:1149065622 cname:VquHLgyd/d3Nd1pO\r\n'}
iceConnectionState: "new"
iceGatheringState: "complete"
localDescription: RTCSessionDescription {type: 'offer', sdp: 'v=0\r\no=- 4764627134364341061 2 IN IP4 127.0.0.1\r\ns…4754 label:f12fee59-268c-4bc3-88c1-8ac27aec8a9c\r\n'}
onaddstream: null
onconnectionstatechange: ƒ (e)
ondatachannel: null
onicecandidate: ƒ (_ref)
onicecandidateerror: ƒ (e)
oniceconnectionstatechange: ƒ ()
onicegatheringstatechange: ƒ (e)
onnegotiationneeded: ƒ ()
onremovestream: null
onsignalingstatechange: null
ontrack: ƒ (_ref3)
pendingLocalDescription: null
pendingRemoteDescription: null
remoteDescription: RTCSessionDescription {type: 'answer', sdp: 'v=0\r\no=- 3069477756847576830 2 IN IP4 127.0.0.1\r\ns…Nd1pO\r\na=ssrc:1149065622 cname:VquHLgyd/d3Nd1pO\r\n'}
sctp: null
signalingState: "stable"
[[Prototype]]: RTCPeerConnection
Here is LiveStream.vue:
<template>
<div>
<main>
<div>
<div id="video-container">
<h2>LiveStream</h2>
<video id="local-video" ref="localVideo" autoplay="true"></video>
</div>
</div>
</main>
<aside>
<div>
<div>
<p>ViewStream</p>
<div v-for="(item, key) in participants" :key="key">
<Video :videoId="key" :videoStream="participants[key].peerStream" />
</div>
<div></div>
</div>
</div>
</aside>
</div>
</template>
<script>
import { videoConfiguration } from "../mixins/WebRTC";
import Video from "../components/Video.vue";
export default {
name: "LiveStream",
components: {
Video,
},
data() {
return {
participants: {},
localStream: null,
pc: null,
roomInfo: {
room: undefined,
username: "testUser",
},
constraints: {
video: {
width: 450,
height: 348,
},
},
};
},
mixins: [videoConfiguration],
methods: {
async initializeWebRTC(user, desc) {
console.log("initializeWebRTC called", { user, desc });
this.participants[user] = {
...this.participants[user],
pc: this.setupRTCPeerConnection(
new RTCPeerConnection(this.configuration),
user,
this.roomInfo.username,
this.roomInfo.room
),
peerStream: null,
peerVideo: null,
};
for (const track of this.localStream.getTracks()) {
this.participants[user].pc.addTrack(track, this.localStream);
console.log("local track added", track);
}
this.createOffer(
this.participants[user].pc,
user,
this.roomInfo.room,
true
);
this.onIceCandidates(
this.participants[user].pc,
user,
this.roomInfo.room,
true
);
},
createPeerConnection() {
this.pc = new RTCPeerConnection(this.configuration);
},
},
created() {
this.roomInfo.room = this.getRoomName();
},
async mounted() {
this.myVideo = document.getElementById("local-video");
await this.getUserMedia();
await this.getAudioVideo();
this.$socket.client.emit("joinRoom", {
...this.roomInfo,
creator: true,
});
},
beforeDestroy() {
this.pc.close();
this.pc = null;
this.$socket.$unsubscribe("newParticipant");
this.$socket.$unsubscribe("onMessage");
this.$socket.client.emit("leaveRoom", {
to: this.to,
from: this.username,
room: this.roomInfo.room,
});
},
sockets: {
connect() {
console.log("connected socket");
},
newParticipant(userObject) {
if (userObject.username === this.roomInfo.username) return;
this.$set(this.participants, userObject.username, {
user: userObject.username,
});
this.initializeWebRTC(userObject.username);
},
async onMessage({ desc, from, room, candidate }) {
if (from === this.username) return;
try {
if (desc) {
const offerCollision =
desc.type === "offer" &&
(this.makingOffer ||
this.participants[from].pc.signalingState !== "stable");
this.ignoreOffer = !this.isPolitePeer && offerCollision;
if (this.ignoreOffer) {
return;
}
if (desc.type === "offer") {
this.handleAnswer(desc, this.participants[from].pc, from, room);
} else {
this.addRemoteTrack(this.participants[from], from);
await this.setRemoteDescription(desc, this.participants[from].pc);
}
} else if (candidate) {
try {
await this.addCandidate(
this.participants[from].pc,
candidate.candidate
);
} catch (err) {
if (!this.ignoreOffer) {
throw err;
}
}
}
} catch (err) {
console.error(err);
}
},
},
};
</script>
Here is the mixin I created to handle a lot of the connection functionality:
export const videoConfiguration = {
data() {
return {
// Media config
constraints: {
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: false
},
video: {
width: 400,
height: 250
}
},
configuration: {
iceServers: [
{
urls: [
"stun:stun1.l.google.com:19302",
"stun:stun2.l.google.com:19302"
]
}
]
},
offerOptions: {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
},
myVideo: null,
localStream: null,
username: null,
isPolitePeer: false,
makingOffer: false,
ignoreOffer: false
};
},
async created() {
this.username = await this.getUsername();
},
beforeDestroy() {
this.localStream.getTracks().forEach((track) => track.stop());
},
methods: {
/**
* Get permission to read from user's microphone and camera.
* Returns audio and video streams to be added to video element
*/
async getUserMedia() {
if ("mediaDevices" in navigator) {
try {
const stream = await navigator.mediaDevices.getUserMedia(
this.constraints
);
if ("srcObject" in this.myVideo) {
this.myVideo.srcObject = stream;
this.myVideo.volume = 0;
} else {
this.myVideo.src = stream;
}
this.localStream = stream;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
}
},
getAudioVideo() {
const video = this.localStream.getVideoTracks();
// eslint-disable-next-line no-console
console.log(video);
const audio = this.localStream.getAudioTracks();
// eslint-disable-next-line no-console
console.log(audio);
},
async setRemoteDescription(remoteDesc, pc) {
try {
await pc.setRemoteDescription(remoteDesc);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
},
addCandidate(pc, candidate) {
try {
const rtcIceCandidate = new RTCIceCandidate(candidate);
pc.addIceCandidate(rtcIceCandidate);
console.log(`${this.username} added a candidate`);
} catch (error) {
console.error(
`Error adding a candidate in ${this.username}. Error: ${error}`
);
}
},
onIceCandidates(pc, to, room) {
pc.onicecandidate = ({ candidate }) => {
if (!candidate) return;
this.$socket.client.emit("new-ice-candidate", {
candidate,
to: to,
from: this.username,
room: room
});
};
},
async createOffer(pc, to, room) {
console.log(`${this.roomInfo.username} wants to start a call with ${to}`);
pc.onnegotiationneeded = async () => {
try {
this.makingOffer = true;
await pc.setLocalDescription();
this.sendSignalingMessage(pc.localDescription, true, to, room);
} catch (err) {
console.error(err);
} finally {
this.makingOffer = false;
}
};
},
async createAnswer(pc, to, room) {
try {
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
this.sendSignalingMessage(pc.localDescription, false, to, room);
} catch (error) {
console.error(error);
}
},
async handleAnswer(desc, pc, from, room) {
await this.setRemoteDescription(desc, pc);
this.createAnswer(pc, from, room);
},
sendSignalingMessage(desc, offer, to, room) {
const isOffer = offer ? "offer" : "answer";
// Send the offer to the other peer
if (isOffer === "offer") {
this.$socket.client.emit("offer", {
desc: desc,
to: to,
from: this.username,
room: room,
offer: isOffer
});
} else {
this.$socket.client.emit("answer", {
desc: desc,
to: to,
from: this.username,
room: room,
offer: isOffer
});
}
},
addRemoteTrack(user, video) {
user.peerVideo = user.peerVideo || document.getElementById(video);
user.pc.ontrack = ({ track, streams }) => {
user.peerStream = streams[0];
track.onunmute = () => {
if (user.peerVideo.srcObject) {
return;
}
user.peerVideo.srcObject = streams[0];
};
};
},
/**
* Using handleRemoteTrack temporarily to add the tracks to the RTCPeerConnection
* for ViewStream since the location of pc is different.
* #param {*} user
*/
handleRemoteTrack(pc, user) {
this.peerVideo = document.getElementById(user);
pc.ontrack = ({ track, streams }) => {
this.peerStream = streams[0];
track.onunmute = () => {
if (this.peerVideo.srcObject) {
return;
}
this.peerVideo.srcObject = streams[0];
};
};
},
setupRTCPeerConnection(pc) {
pc.onconnectionstatechange = (e) => {
console.log(
"WebRTC: Signaling State Updated: ",
e.target.signalingState
);
};
pc.oniceconnectionstatechange = () => {
console.log("WebRTC: ICE Connection State Updated");
};
pc.onicegatheringstatechange = (e) => {
console.log(
"WebRTC: ICE Gathering State Updated: ",
e.target.iceGatheringState
);
};
pc.onicecandidateerror = (e) => {
if (e.errorCode === 701) {
console.log("ICE Candidate Error: ", e);
}
};
return pc;
}
}
};
I created a CodeSandbox that has the ViewStream.vue file and the directory structure for how I'm trying to set it up. (It's just too much code to post here.)
When the viewer joins the room created by the streamer, I can see that they exchange offer/answer and ice candidates. However, I still do not see any change in the connectionState or iceConnectionState. Is there a piece that I'm not doing?
One thing I noticed when logging data and digging through chrome://webrtc-internals/ is that the MediaStream ID's don't match.
I log out the tracks after the call to getUserMedia(), and note the track ID's.
This image shows the stream IDs for the caller (top) and the callee (bottom)
I then log when I'm adding the local tracks to the RTCPeerConnection, and they match what was generated for both peers.
Here, the tracks for the streamer are added to the RTCPeerConnection. The IDs match from above.
However, I'm also logging for each peer when I receive a remote track, and that's when the ID's don't match.
I don't know what is generating the ID in this picture. It's different from the ID of the callee in the first picture.
Is that normal behavior? Would the fact that the IDs don't match be the cause of the streams not starting on either end? I don't know what would cause this. The IDs are the same when added to the RTCPeerConnection on either end of the call.
Edit 5/1: I removed the TURN server from my config, and that fixed part of the connection process. Still having a problem getting media to flow between peers. But I can see that I've captured a MediaStream on each side of the connection.
It looks like the connectionState is not updating. There maybe a race condition between creating the offer and answer that is not allowing connectionState to update.
You may want see look into adding a promise on creating the offer and answer, and on when the ice candidates are completed to handle this race condition.
Can I have the Server side code? So that I can recreate whats going on?
Edit:
If you are talking about this in your second question.
I guess it just explains how the collision between a polite and impolite peer is resolved by polite peer becoming callee from caller.
And that is just the definition of Polite Peer and I thing you don't need to worry much about explicitly changing the roles in this context.
A polite peer, essentially, is one which may send out offers, but then responds if an offer arrives from the other peer with "Okay, never mind, drop my offer and I'll consider yours instead. ~ MDN Docs
I hope this answers your Second Question
Found the solution:
I was getting this error: TypeError: Failed to execute 'addIceCandidate' on 'RTCPeerConnection': The provided value is not of type 'RTCIceCandidateInit'.
Googling that led me to this SO article which led me to believe that it was an error that I could safely ignore.
However, the MDN docs said that I'd get a TypeError if I was missing a required part of the object, and that led me to realize that on both sides of the call I was destructuring {candidate}, passing in an incomplete object. So the candidates that were being passed to each party weren't really being added. Once I fixed that everything worked.
I have this piece of code to be tested:
electronApp.on('ready', async () => {
const filter = {
urls: ['*://*.company.com/*'],
};
try {
session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
details.requestHeaders['Origin'] = '*';
callback({ requestHeaders: details.requestHeaders });
});
} catch (error) {
log.error(error.message);
}
start(electronApp);
});
I have created unit test for the 'start main process' like this:
import { App as MockApp } from '../../__mocks__/electron'; // This mocks partially electron module
...
it('should start the main process', () => {
const mockApp = app as unknown as jest.Mocked<App>;
mockApp.emit('ready');
expect(mockApp.requestSingleInstanceLock).toHaveBeenCalledTimes(1);
});
It works. but I don't find a way to test that try/catch and the session -> onBeforeSendHeaders.
Any suggestion? Thanks a lot.
Finally it worked:
import { App as MockApp } from '../../__mocks__/electron';
...
it('should start the main process', (done) => {
const mockApp = app as unknown as jest.Mocked<App>;
const mockCb = jest.fn();
session.defaultSession.webRequest.onBeforeSendHeaders = (filter: string[], cb: (details, reqCb) => void): void => {
cb({ url: '*://*.company.com/*', requestHeaders: {} }, mockCb);
expect(mockCb).toHaveBeenCalledTimes(1);
expect(mockCb).toHaveBeenCalledWith({ requestHeaders: { Origin: '*' } });
done();
};
mockApp.emit('ready');
});
I also mocked some functions of electron library mocks__/electron.ts,
export const session = {
defaultSession: {
webRequest: {
onBeforeSendHeaders: (filter: string[], cb: (details, reqCb) => void): void => {
cb({ url: 'http"//some/site.com' }, jest.fn());
},
},
},
};
Just in case someone has similar issue.
As I am new in Vuejs, It will be very easy for others, but for me I cannot get it right, I search a lot but cannot get the expected answer
Below is my code
watch: {
$route () {
this.newsData = []
this.loadNewsByCategory()
this.category = {}
this.getCategoryData()
}
},
created () {
this.getCategoryData()
this.loadNewsByCategory()
},
methods () {
async getCategoryData() {
// console.log('called category data')
try {
await firebase.firestore().collection('categories').doc(this.$route.params.id).get().then((doc) => {
if (doc.exists) {
this.category = doc.data()
}
})
} catch (error) {
console.log(error) // this line show the error I've post below
this.$q.notify({ type: 'negative', message: error.toString() })
}
},
async loadNewsByCategory() {
// console.log('load news')
try {
var db = firebase.firestore()
var first = db.collection('posts')
.where('publishedAt', '<=', new Date())
.where('categories', 'array-contains', this.$route.params.id)
.orderBy('publishedAt', 'desc')
.limit(12)
return await first.get().then((documentSnapshots) => {
documentSnapshots.forEach((doc) => {
const news = {
id: doc.id,
slug: doc.data().slug,
title: doc.data().title,
image: doc.data().image.originalUrl,
publishedAt: dateFormat(doc.data().publishedAt.toDate(), 'dS mmm, yyyy')
}
this.newsData.push(news)
})
this.lastVisible = documentSnapshots.docs[documentSnapshots.docs.length - 1]
})
} catch (error) {
this.$q.notify({ type: 'negative', message: error.toString() })
}
}
}
In my code, when initialize I call two functions to query data from firebase and it's working as expected. but when the route change for example: from getdata/1 to getdata/2 one function i.e., loadNewsByCategory() is working but others throw error like below
TypeError: u.indexOf is not a function
at VueComponent.getCategoryData (getdata.vue?3b03:102)
at VueComponent.created (getdata.vue?3b03:92)
Thank you in advance
I'm mocking the #elastic/elasticsearch library and I want to test that the search method is called with the right arguments but I'm having issues accessing search from my tests.
In my ES mock I just export an object that includes a Client prop that returns another object that has the search prop. This is the way search is accessed from the library
const { Client } = require('#elastic/elasticsearch')
const client = new Client(...)
client.search(...)
__mocks__/#elastic/elasticsearch
module.exports = {
Client: jest.fn().mockImplementation(() => {
return {
search: (obj, cb) => {
return cb(
'',
{
statusCode: 200,
body: {
hits: {
hits: [
{
_source: esIndexes[obj.index]
}
]
}
}
}
)
}
}
})
}
__tests__/getAddresses.test.js
const { getAddresses } = require('../src/multiAddressLookup/utils/getAddresses')
const { Client } = require('#elastic/elasticsearch')
beforeEach(() => {
process.env.ES_CLUSTER_INDEX = 'foo'
process.env.ui = '*'
})
describe('multiAddressLookup', () => {
test('Should return the correct premises data with only the relevant "forecasted_outages"', async () => {
const event = {
foo: 'bar'
}
const esQueryResponse = {
"body": "\"foo\":\"bar\"",
"headers": {"Access-Control-Allow-Origin": '*'},
"statusCode": 200
}
await expect(getAddresses(event)).resolves.toEqual(esQueryResponse)
expect(Client().search).toHaveBeenCalled() // This fails with 0 calls registered
})
})
I'm not sure of any exact documentation for this scenario but I got the idea while looking through the Jest: The 4 ways to create an ES6 Mock Class - Automatic mock portion of the Jest documentation.
First, the search method in the ES mock, __mocks__/#elastic/elasticsearch, needs to be converted into a jest mock function, jest.fn(). Doing this gives us access to properties and values that jest mocks provide.
__mocks__/#elastic/elasticsearch.js converted
module.exports = {
Client: jest.fn().mockImplementation(() => {
return {
search: jest.fn((obj, cb) => {
return cb(
'',
{
statusCode: 200,
body: {
hits: {
hits: [
{
_source: esIndexes[obj.index]
}
]
}
}
}
)
})
}
})
}
Second, in our tests we need to follow the path from the Client mock class until we find out methods. The syntax is MockClass.mock.results[0].value.mockFunction.
Example Test
const { Client } = require('#elastic/elasticsearch') // This is located in the "__mocks__" folder in the root of your project
const { getAddresses } = require('../../src/getAddresses') // This is the file we wrote and what we are unit testing
describe('getAddresses', () => {
it('Should call the ES Search method', async () => {
const event = { ... }
const expected = { ... }
await expect(getAddresses(event)).resolves.toEqual(expected) // pass
expect(Client.mock.results[0].value.search).toHaveBeenCalled() // pass
})
})