I use sip.js [onsip.com] library like web sip client.
Is it possible to make connection with linphone sip service.
Code:
/* eslint-disable #typescript-eslint/no-explicit-any */
// eslint-disable-next-line #typescript-eslint/no-unused-vars
import { UserAgentOptions, Web } from "sip.js";
import { SimpleUserDelegate } from "sip.js/lib/platform/web/simple-user/simple-user-delegate";
import { getButton, getE, getVideo } from "./scripts/demo-utils";
const domainOnSip = "sipjs.onsip.com";
export let webSocketServer="wss://edge.sip.onsip.com";
// eslint-disable-next-line #typescript-eslint/no-unused-vars
const domainLinphone = 'sip.linphone.org';
const webSocketServerLinphone="wss://sip.linphone.org";
const iamUser = "nidza";
webSocketServer = webSocketServerLinphone;
console.info('Hackey running...', Web);
const delegate: SimpleUserDelegate={
onCallReceived: async () => {
console.log('Incoming Call!');
await simpleUser2.answer();
}
};
const userAgentOptions = {
sessionDescriptionHandlerFactoryOptions: {
peerConnectionConfiguration: {
iceServers: [{
urls: "stun:stun.l.google.com:19302"
}, {
username: "nidza",
credential: "***********"
}]
},
}
};
// Options for SimpleUser
const options: Web.SimpleUserOptions={
userAgentOptions: userAgentOptions,
aor: `sip:${iamUser}#${domainLinphone}`,
media: {
constraints: { audio: false, video: true },
local: {
video: getVideo("videoLocalAlice")
},
remote: {
video: getVideo("videoRemoteAlice")
}
}
};
const options2: Web.SimpleUserOptions={
aor: `sip:zlatnaspirala#${domainLinphone}`,
media: {
constraints: { audio: false, video: true },
local: {
video: getVideo("videoLocalBob")
},
remote: {
video: getVideo("videoRemoteBob")
}
},
delegate: delegate
};
// Construct a SimpleUser instance
const simpleUser=new Web.SimpleUser(webSocketServer, options);
const simpleUser2=new Web.SimpleUser(webSocketServer, options2);
simpleUser.connect()
.then(() => {
console.info("status: sip server connected.");
(getE('callIdList') as HTMLInputElement).disabled=false;
(getButton('connectUser')).disabled=true;
(getButton('registerUser')).disabled=false;
simpleUser.register().then(() => {
console.info("status: registered.");
getButton('registerUser').disabled=true;
getButton('callBtn').disabled=false;
getButton('callBtn').classList.add('cooliano');
getButton('callBtn').addEventListener('click', (e) => {
console.log(">>>>"+(getE('callIdList') as HTMLSelectElement).selectedOptions[0].innerText)
console.log(">>>>"+(getE('callIdList') as HTMLSelectElement).selectedIndex)
const emailLocal=(getE('callIdList') as HTMLSelectElement).selectedOptions[0].innerText;
const curAddress=`sip:${emailLocal}`;
// const curAddress = `sip:bob#${domain}`;
simpleUser.call(curAddress, undefined, {
requestDelegate: {
onAccept: (e) => {
console.info(`status: ${(e as any).message.from.uri.normal.user} INVITE accepted from ${(e as any).message.to.uri.normal.user}`);
getButton('hangupBtn').disabled=false;
(getButton('hangupBtn')).addEventListener('click', () => {
//
simpleUser.hangup().then(() => {
console.log("just test hangup")
})
})
},
onReject: (response) => {
console.info(`status: INVITE rejected`);
let message=`Session invitation to rejected.\n`;
message+=`Reason: ${response.message.reasonPhrase}\n`;
alert(message);
}
},
withoutSdp: false
})
.catch((error: Error) => {
console.error(`error: failed to begin session`, error);
alert(` Failed to begin session.\n`+error);
});
})
}).catch((error: Error) => {
console.error("error in registration:", error);
});
}).catch((error: Error) => {
console.error("error in connection:", error);
});
simpleUser2.connect()
.then(() => {
simpleUser2.register().then(() => {
console.log("Nice reg bob");
}).catch((error: Error) => {
console.log("Error in registration:", error);
});
}).catch((error: Error) => {
console.log("Error:", error);
});
transport.js:197 WebSocket connection to 'wss://sip.linphone.org/' failed:
_connect # transport.js:197
logger-factory.js:85 Sat Jan 07 2023 21:23:45 GMT+0100 (Central European Standard Time) | sip.Transport | WebSocket error occurred.
print # logger-factory.js:85
hackeyWeb.ts:108 error in connection: Error: WebSocket closed wss://sip.linphone.org (code: 1006)
at Transport.onWebSocketClose (transport.js:337:1)
at WebSocket.<anonymous> (transport.js:199:1)
Any suggestion ?
You won't be able to do that, flexisip doesn't currently support websockets.
Related
So this is the scenario / premises:
In order to populate a chat queue in real time I need to open a connection to a websocket, send a message and then set the data to a websocket store. This store will basically manage all the websocket state.
Before populating the chat queue there's two parameters I need: a shiftId coming from one http API request and a connectionId coming from the websocket. Using those two parameters I finally can subscribe to a third http API and start receiving messages to populate the chat queue.
The problem is that due to the async behaviour of the websocket (or that's what I think, please feel to correct me if I'm wrong) I always get an empty "connectionId" when trying to make the put to that "subscription" API. I have tried with async/await and promises but nothing seems to work. I'm pretty new to async/await and websockets with Vuex so pretty sure I'm doing something wrong.
This is the user vuex module where I do all the login/token operations and dispatch a "updateEventsSubscription" action from the shift vuex module. In order for the "updateEventsSubscription" action to work I need to get the response from the "processWebsocket" action (to get the connectionId parameter) and from the "startShift" action (to get the shiftId parameter) coming from the shifts vuex module:
import UserService from '#/services/UserService.js'
import TokenService from '#/services/TokenService.js'
import router from '#/router'
export const namespaced = true
export const state = {
accessToken: '',
errorMessage: '',
errorState: false,
userEmail: localStorage.getItem('userEmail'),
userPassword: localStorage.getItem('userPassword'),
}
export const mutations = {
SET_TOKEN(state, accessToken) {
state.accessToken = accessToken
TokenService.saveToken(accessToken)
},
SET_USER(state, authUserJson) {
state.userEmail = authUserJson.email
state.userPassword = authUserJson.password
localStorage.setItem('userPassword', authUserJson.password)
localStorage.setItem('userEmail', authUserJson.email)
},
SET_ERROR(state, error) {
state.errorState = true
state.errorMessage = error.data.error_description
},
CLOSE_NOTIFICATION(state, newErrorState) {
state.errorState = newErrorState
},
}
export const actions = {
signIn({ commit, dispatch, rootState }, authUserJson) {
return UserService.authUser(authUserJson)
.then((result) => {
commit('SET_USER', authUserJson)
commit('SET_TOKEN', result.data.access_token)
dispatch('token/decodeToken', result.data.access_token, {
root: true,
})
dispatch(
'shifts/updateEventsSubscription',
rootState.token.agentId,
{
root: true,
}
)
router.push('/support')
})
.catch((error) => {
console.log(error)
if (error.response.status === 400) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
commit('SET_ERROR', error.response)
} else {
console.log(error.response)
}
})
},
signOut({ commit }) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
localStorage.removeItem('userPassword')
localStorage.removeItem('userEmail')
TokenService.removeToken()
router.push('/')
},
closeNotification({ commit }, newErrorState) {
commit('CLOSE_NOTIFICATION', newErrorState)
},
}
export const getters = {
getToken: (state) => {
return state.accessToken
},
errorState: (state) => {
return state.errorState
},
errorMessage: (state) => {
return state.errorMessage
},
isAuthenticated: (state) => {
return state.accessToken
},
userEmail: (state) => {
return state.userEmail
},
userPassword: (state) => {
return state.userPassword
},
}
This is websocket store: I pass the connectionId to the state in order to be able to use it in another vuex action to subscribe for new chats:
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChatInfo: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
},
}
export const mutations = {
SET_REMOTE_DATA(state, remoteData) {
const wsData = JSON.parse(remoteData.data)
if (wsData.connectionId) {
state.connectionId = wsData.connectionId
console.log(`Retrieving Connection ID ${state.connectionId}`)
} else {
console.log(`We got chats !!`)
state.messageType = wsData.type
state.incomingChatInfo = wsData.documents
}
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
And finally this is the shift store (where the problem is), as you can see I have a startShift action (everything works fine with it) and then the "updateEventsSubscription" where I'm trying to wait for the response from the "startShift" action and the "processWebsocket" action. Debugging the app I realize that everything works fine with the startShift action but the websocket action sends the response after the "updateEventsSubscription" needs it causing an error when I try to make a put to that API (because it needs the connectionId parameter coming from the state of the websocket).
import ShiftService from '#/services/ShiftService.js'
export const namespaced = true
export const state = {
connectionId: '',
shiftId: '',
agentShiftInfo: '{}',
}
export const actions = {
startShift({ commit }, agentId) {
return ShiftService.startShift(agentId)
.then((response) => {
if (response.status === 200) {
commit('START_SHIFT', response.data.aggregateId)
}
})
.catch((error) => {
console.log(error)
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
async updateEventsSubscription({ dispatch, commit, rootState }, agentId) {
await dispatch('startShift', agentId)
const shiftId = state.shiftId
await dispatch('websocket/processWebsocket', null, { root: true })
let agentShiftInfo = {
aggregateId: state.shiftId,
connectionId: rootState.websocket.connectionId,
}
console.log(agentShiftInfo)
return ShiftService.updateEventsSubscription(shiftId, agentShiftInfo)
.then((response) => {
commit('UPDATE_EVENTS_SUBSCRIPTION', response.data)
})
.catch((error) => {
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
}
export const mutations = {
START_SHIFT(state, shiftId) {
state.shiftId = shiftId
console.log(`Retrieving Shift ID: ${state.shiftId}`)
},
UPDATE_EVENTS_SUBSCRIPTION(state, agentShiftInfo) {
state.agentShiftInfo = agentShiftInfo
},
}
You should convert your WebSocket action into a promise that resolves when WebSocket is connected.:
export const actions = {
processWebsocket({ commit }) {
return new Promise(resolve=> {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
resolve();
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
});
},
}
So I realized that I have to resolve the promise on the this.ws.message instead. By doing that all my data is populated accordingly, there's still sync issues (I can't feed the websocket state at the moment because due to its async behaviour the state is not there yet when other components try to use it via: rootGetters.websocket.incomingChats for example) but I guess that's part of another question. Here's the final version of the module action:
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('wss://ws.rubiko.io')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
Anyways, thanks #Eldar you were in the right path.
I have real hard time to get custom Apollo error on the client side.
Here is the server code:
...
const schema = makeExecutableSchema({
typeDefs: [constraintDirectiveTypeDefs, ...typeDefs],
resolvers,
schemaTransforms: [constraintDirective()],
});
const server = new ApolloServer({
schema,
dataSources,
context({ req }) {
const token = req.headers.authorization;
const user = token ? getUserFromToken(token) : '';
return { user };
},
debug: false,
formatError: (err) => {
// ToDo: Generate unique token and log error
if (err!.extensions!.code == 'INTERNAL_SERVER_ERROR') {
return new ApolloError('We are having some trouble', 'ERROR', {
token: 'uniquetoken',
});
}
return err;
},
uploads: false,
});
...
Client code:
...
const ADD_CLAIM = gql`
mutation addClaim($claim: ClaimInput!) {
addClaim(claim: $claim) {
id
}
}
`;
...
const [addClaim, { data, error }] = useMutation(ADD_CLAIM);
...
const onSubmit = async () => {
try {
debugger;
const r = await addClaim({
variables: {
input: {
id: insured.insured,
date: '20/12/2020',
...
therapy: treatment.treatments.map(treat => ({
id: treat.treatId,
...
})),
},
},
});
debugger;
console.log('r', r);
} catch (err) {
debugger;
setFormError(error ? error.message : err.message);
console.log('Error:', err);
}
};
...
if (error) {
debugger;
return <div>error</div>;
}
I expect to get the custom error : "We are having some trouble".
However, no matter what I do I got: "Response not successful: Received status code 400"
I am 100% give custom error from the server:
But I receive on client side:
Moreover, when I check network tab of Developer Tools, response I do have my error:
But I cannot access it from the code.
BTW, in the playground I see my error:
Here where are my errors :
error.networkError.result.errors
What nobody knows ?
Or
const errorLink = onError(({ graphQLErrors, networkError }) => {
debugger;
console.log(graphQLErrors);
console.log(networkError);
});
const client = new ApolloClient({
...
link: ApolloLink.from( [errorLink, ...]),
});
It works as well.
Yes, sometimes GraphQL is a nasty beast
I am using expo for react-native application. I wanted to add a functionality where I could show notification when actual time will be the same as a hardcoded time in code. The problem is that it works when I have my app in expo opened but doesn't when it is in background.
Here is the notifications handler that should run it in background:
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
Here is my useEffect for setting push notifications:
useEffect(() => {
registerForPushNotificationsAsync().then((token: any) => {
setExpoPushToken(token);
});
notificationListener.current = Notifications.addNotificationReceivedListener(
(notification: any) => {
setNotification(notification);
}
);
responseListener.current = Notifications.addNotificationResponseReceivedListener(
(response) => {
console.log(response);
}
);
return () => {
Notifications.removeNotificationSubscription(notificationListener);
Notifications.removeNotificationSubscription(responseListener);
};
}, []);
And here is the code for registering and sending notifications with token:
async function sendPushNotification() {
await Notifications.scheduleNotificationAsync({
content: {
title: "You've got mail! 📬",
body: "Here is the notification body",
data: { data: "goes here" },
},
trigger: { seconds: 1 },
});
}
async function registerForPushNotificationsAsync() {
let token;
if (Constants.isDevice) {
const { status: existingStatus } = await Permissions.getAsync(
Permissions.NOTIFICATIONS
);
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Permissions.askAsync(
Permissions.NOTIFICATIONS
);
finalStatus = status;
}
if (finalStatus !== "granted") {
alert("Failed to get push token for push notification!");
return;
}
token = (await Notifications.getExpoPushTokenAsync()).data;
console.log(token);
}
else {
alert("Must use physical device for Push Notifications");
}
return token;
}
So I just use sendPushNotification() function for sending it. It works when I the app is open but after minimising it it doesn't show anything. How can I fix this?
I have the following controller
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { UserModel, isPasswordAllowed } from '../../models/User';
const saltRounds = 10;
function userController() {
function add(req, res) {
try {
if (req.body.administrator) {
res.status(400).json({
error: {
message: 'Bad Request',
},
});
return;
}
if (!isPasswordAllowed(req.body.password)) {
res.status(400).json({
error: {
message: 'La contraseña no cumple con los requisitos minimos',
},
});
return;
}
bcrypt.hash(req.body.password, saltRounds, async (err, hash) => {
if (err) {
res.status(500).json({ error: { code: '500', message: err.errmsg } });
return;
}
const user = new UserModel();
user.email = req.body.email.toLowerCase();
user.password = hash;
await user
.save()
.then(() => {
const token = jwt.sign(
{
username: user.email,
userId: user.id,
},
process.env.JWT_KEY,
{
expiresIn: '7d',
},
);
res.status(200).json({
message: 'Usuario Creado',
token,
email: user.email,
});
})
.catch((error) => {
if (error.code === 11000) {
res.status(400).json({
error: { code: '500', message: 'El correo ya existe' },
});
} else {
console.log(error);
res.status(500).json({ error: { code: '500', message: error.message } });
}
});
});
} catch (error) {
res.status(503).json({ error });
}
}
return {
add,
};
}
export default userController();
As you can expect this controller works great, the user is created in the database, but I have the following test:
import UserController from './UserController';
import { connect, closeDatabase, clearDatabase } from '../../__test__/db-handler';
describe('test UserController', () => {
const res = {};
beforeEach(async () => {
await connect();
res.send = jest.fn().mockReturnValue(res);
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
});
afterEach(async () => {
await clearDatabase();
});
afterAll(async () => {
await closeDatabase();
});
test('should return the expect api method', () => {
const userControllerApi = {
add: expect.any(Function),
};
expect(UserController).toMatchObject(userControllerApi);
});
test('should return 400 error bad request is body contains administrator: true', async () => {
const req = {
body: {
administrator: true,
},
};
await UserController.add(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledTimes(1);
expect(res.json).toHaveBeenCalledWith({
error: {
message: 'Bad Request',
},
});
});
test('should return 400 error bad request is password is not allow', async () => {
const req = {
body: {
password: '123456',
},
};
await UserController.add(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledTimes(1);
expect(res.json).toHaveBeenCalledWith({
error: {
message: 'La contraseña no cumple con los requisitos minimos',
},
});
});
// this test is not passing
test('should create an user and return a token', async () => {
const req = {
body: {
email: 'test#test.com',
password: 'Abc123456',
},
};
const expectObject = {
message: 'Usuario Creado',
email: 'test#test.com',
};
await UserController.add(req, res);
jest.useFakeTimers();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledTimes(1);
expect(res.json).toMatchObject(expectObject);
});
});
but the last test 'should create an user and return a token' never pass and I get the following:
● test UserController › should create an user and return a token
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: 200
Number of calls: 0
78 | jest.useFakeTimers();
79 |
> 80 | expect(res.status).toHaveBeenCalledWith(200);
| ^
81 | expect(res.json).toHaveBeenCalledTimes(1);
82 | expect(res.json).toMatchObject(expectObject);
83 | });
I also debbug this code in testing mode and as you can see in the following image, the code is enter in the res.status(200).json({ .... }), so I don't understand what it is happening here.
The problem is that you're mixing callbacks with async/await, meaning that execution of add() will be finished before the callback of bcrypt.hash has been finished. This results in res.status not to have been called yet in your test.
You can fix this by awaiting the bcrypt.hash call (it supports returning a promise by default):
// await hashing function instead of using callback
const hash = await bcrypt.hash(req.body.password, saltRounds);
const user = new UserModel();
user.email = req.body.email.toLowerCase();
user.password = hash;
// rest of the code ...
I am trying to connect two peers using webRTC. I am able to display both local and remote videos correctly but as soon as the remote video appears, the candidate object becomes null and on the console it logs the following error message.
TypeError: Failed to execute 'addIceCandidate' on 'RTCPeerConnection': Candidate missing values for both sdpMid and sdpMLineIndex
I am using two separate laptops to test the connection and since both remote and local videos are showing, I think that I have connected two peers successfully but I'm not sure because of the error message.
Any idea on why this might be happening? Am I even successfully connecting the two peers?
Below is the code.
Thank you!
Frontend
import React, { Component } from 'react';
import io from 'socket.io-client';
class App extends Component {
constructor(props) {
super(props);
this.room = 'test-room';
this.socket = io.connect('http://localhost:5000');
this.localPeerConnection = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
});
this.remotePeerConnection = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
});
};
componentDidMount() {
this.socket.on('connect', () => {
this.socket.emit('join', this.room, err => {
if (err) {
console.error(err);
} else {
this.socket.on('offer', offer => {
console.log('OFFER RECEIVED: ', offer);
this.createAnswer(offer);
});
this.socket.on('candidate', candidate => {
console.log('CANDIDATE RECEIVED', candidate);
this.localPeerConnection.addIceCandidate(candidate).catch(error => console.error(error));
this.remotePeerConnection.addIceCandidate(candidate).catch(error => console.error(error));
});
this.socket.on('answer', answer => {
console.log('ANSWER RECEIVED:', answer);
this.localPeerConnection.setRemoteDescription(answer);
});
}
});
});
}
startCall = async () => {
this.localPeerConnection.onicecandidate = e => {
const iceCandidate = e.candidate;
this.socket.emit('candidate', { room: this.room, candidate: iceCandidate });
console.log('candidate generated', e.candidate);
};
this.localPeerConnection.ontrack = e => {
this.remoteVideo.srcObject = e.streams[0];
console.log('REMOTE STREAM?: ', e.streams[0]);
};
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 150, height: 150 }, audio: false });
for (const track of stream.getTracks()) {
this.localPeerConnection.addTrack(track, stream);
}
this.localVideo.srcObject = stream;
console.log('LOCAL STREAMS: ', this.localPeerConnection.getLocalStreams())
return this.createOffer();
} catch (error) {
console.error(error);
}
}
createOffer = async () => {
try {
const offer = await this.localPeerConnection.createOffer();
await this.localPeerConnection.setLocalDescription(offer);
await this.remotePeerConnection.setRemoteDescription(offer);
this.socket.emit('offer', { room: this.room, offer });
console.log('SENDING OFFER: ', offer);
} catch (error) {
console.error(error);
}
}
createAnswer = async description => {
this.remotePeerConnection.onicecandidate = e => {
const iceCandidate = e.candidate;
this.socket.emit('candidate', { room: this.room, candidate: iceCandidate });
console.log('candidate generated', e.candidate);
};
this.remotePeerConnection.ontrack = e => {
this.remoteVideo.srcObject = e.streams[0];
};
this.remotePeerConnection.setRemoteDescription(description)
.then(() => navigator.mediaDevices.getUserMedia({ video: { width: 150, height: 150 }, audio: false }))
.then(stream => {
for (const track of stream.getTracks()) {
this.remotePeerConnection.addTrack(track, stream);
}
this.localVideo.srcObject = stream;
return this.remotePeerConnection.createAnswer();
})
.then(answer => {
this.remotePeerConnection.setLocalDescription(answer);
return answer;
})
.then(answer => {
this.socket.emit('answer', { room: this.room, answer });
console.log('SENDING ANSWER: ', answer);
})
.catch(error => console.error(error))
}
render() {
return (
<div>
<h1>Webrtc</h1>
<div>
<button onClick={this.startCall}>CALL</button>
</div>
<div style={{ display: 'flex' }}>
<div>
<video id='localVideo' autoPlay muted playsInline ref={ref => this.localVideo = ref} />
<p>LOCAL VIDEO</p>
</div>
<div>
<video id='remoteVideo' autoPlay muted playsInline ref={ref => this.remoteVideo = ref} />
<p>REMOTE VIDEO</p>
</div>
</div>
</div>
);
}
}
export default App;
Server
const express = require('express');
const app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server);
const PORT = process.env.PORT || 5000;
const connections = [];
const clients = [];
io.set('origins', '*:*');
io.on('connection', socket => {
connections.push(socket);
clients.push({ socket_id: socket.id });
console.log('Connected: %s sockets connected ', connections.length);
socket.on('join', (room, callback) => {
const clients = io.sockets.adapter.rooms[room];
const numClients = (typeof clients !== 'undefined') ? clients.length : 0;
console.log('joined room', room);
if (numClients > 1) {
return callback('already_full');
}
else if (numClients === 1) {
socket.join(room);
io.in(room).emit('ready');
}
else {
socket.join(room);
}
callback();
});
socket.on('offer', (data) => {
const { room, offer } = data;
console.log('offer from: ', offer);
socket.to(room).emit('offer', offer);
});
socket.on('answer', (data) => {
const { room, answer } = data;
console.log('answer from: ', answer);
socket.to(room).emit('answer', answer);
});
socket.on('candidate', (data) => {
const { room, candidate } = data;
console.log('candidate: ', candidate);
socket.to(room).emit('candidate', candidate);
});
socket.on('disconnect', () => {
connections.splice(connections.indexOf(socket), 1);
console.log('Disconnected: %s sockets connected, ', connections.length);
clients.forEach((client, i) => {
if (client.socket_id === socket.id) {
clients.splice(i, 1);
}
});
});
});
server.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
UPDATE
After reading jib's comment, I have modified my client js as follows.
import React, { Component } from 'react';
import io from 'socket.io-client';
class App extends Component {
constructor(props) {
super(props);
this.room = 'test-room';
this.socket = io.connect('http://localhost:5000');
this.peerConnection = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
});
};
componentDidMount() {
this.socket.on('connect', () => {
this.socket.emit('join', this.room, err => {
if (err) {
console.error(err);
} else {
this.socket.on('offer', offer => {
console.log('OFFER RECEIVED: ', offer);
this.createAnswer(offer);
});
this.socket.on('candidate', candidate => {
this.peerConnection.addIceCandidate(candidate).catch(error => console.error(error));
console.log('CANDIDATE RECEIVED', candidate);
});
this.socket.on('answer', answer => {
console.log('ANSWER RECEIVED:', answer);
this.peerConnection.setRemoteDescription(answer);
});
}
});
});
}
startCall = async () => {
this.peerConnection.oniceconnectionstatechange = () => console.log('ICE CONNECTION STATE: ', this.peerConnection.iceConnectionState);
this.peerConnection.onicecandidate = e => {
const iceCandidate = e.candidate;
this.socket.emit('candidate', { room: this.room, candidate: iceCandidate });
console.log('candidate generated', e.candidate);
};
this.peerConnection.ontrack = e => {
this.remoteVideo.srcObject = e.streams[0];
console.log('REMOTE STREAMS: ', this.peerConnection.getRemoteStreams());
};
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 150, height: 150 }, audio: false });
for (const track of stream.getTracks()) {
this.peerConnection.addTrack(track, stream);
}
this.localVideo.srcObject = stream;
console.log('LOCAL STREAMS: ', this.peerConnection.getLocalStreams())
return this.createOffer();
} catch (error) {
console.error(error);
}
}
createOffer = async () => {
try {
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
this.socket.emit('offer', { room: this.room, offer });
console.log('SENDING OFFER: ', offer);
} catch (error) {
console.error(error);
}
}
createAnswer = async description => {
this.peerConnection.onicecandidate = e => {
const iceCandidate = e.candidate;
this.socket.emit('candidate', { room: this.room, candidate: iceCandidate });
console.log('candidate generated', e.candidate);
};
this.peerConnection.ontrack = e => {
this.remoteVideo.srcObject = e.streams[0];
};
this.peerConnection.setRemoteDescription(description)
.then(() => navigator.mediaDevices.getUserMedia({ video: { width: 150, height: 150 }, audio: false }))
.then(stream => {
for (const track of stream.getTracks()) {
this.peerConnection.addTrack(track, stream);
}
this.localVideo.srcObject = stream;
return this.peerConnection.createAnswer();
})
.then(answer => {
this.peerConnection.setLocalDescription(answer);
return answer;
})
.then(answer => {
this.socket.emit('answer', { room: this.room, answer });
console.log('SENDING ANSWER: ', answer);
})
.catch(error => console.error(error))
}
render() {
return (
<div>
<h1>Webrtc</h1>
<div>
<button onClick={this.startCall}>CALL</button>
</div>
<div style={{ display: 'flex' }}>
<div>
<video id='localVideo' autoPlay muted playsInline ref={ref => this.localVideo = ref} />
<p>LOCAL VIDEO</p>
</div>
<div>
<video id='remoteVideo' autoPlay muted playsInline ref={ref => this.remoteVideo = ref} />
<p>REMOTE VIDEO</p>
</div>
</div>
</div>
);
}
}
export default App;
The error on my console still persists... any idea why?
The error on my console still persists... any idea why?
This is a known bug in Chrome (please ★ it to have it fixed!)
To see it, type the following into web console in Chrome 78:
const pc = new RTCPeerConnection(); pc.setRemoteDescription(await pc.createOffer())
then
pc.addIceCandidate(undefined)
and it produces:
TypeError: Failed to execute 'addIceCandidate' on 'RTCPeerConnection':
Candidate missing values for both sdpMid and sdpMLineIndex
Now try
pc.addIceCandidate()
and it says:
TypeError: Failed to execute 'addIceCandidate' on 'RTCPeerConnection':
1 argument required, but only 0 present.
Both are in violation of the latest spec, which says that pc.addIceCandidate() is "an end-of-candidates indication" that "applies to all media descriptions."
Workaround
You can safely ignore this error until Chrome fixes it, or catch the TypeError and suppress it.
I recommend against if (candidate) pc.addIceCandidate(candidate) as a workaround, as once Chrome fixes this, it would prevent iceConnectionState from ever going to the "completed" state in Chrome or any other browser.