Reputation: 3528
I would like connect 2 devices with WebRTC on localhost. All devices have no internet access. They are connected to a same local wifi.
I try this on React Native App.
In this context offline, do I need to trickle ICE candidates and addIceCandidate
? If I understund correctly, ICE candidates is for iceServer
. But my case, iceServer
is null (because i'm offline only, connected on same localhost wifi) :
const configuration = { iceServers: [{ urls: [] }] };
So actualty i exchange offer and answer, but after setRemoteDescription
the answer, the connectionState
stay on checking
.
You can see my React Component :
constructor(props) {
super(props);
this.pc = new RTCPeerConnection(configuration);
}
state = initialState;
componentDidMount() {
const { pc } = this;
if (pc) {
this.setState({
peerCreated: true
});
}
this.setConnectionState();
pc.oniceconnectionstatechange = () => this.setConnectionState();
pc.onaddstream = ({ stream }) => {
if (stream) {
this.setState({
receiverVideoURL: stream.toURL()
});
}
};
pc.onnegotiationneeded = () => {
if (this.state.initiator) {
this.createOffer();
}
};
pc.onicecandidate = ({ candidate }) => {
if (candidate === null) {
const { offer } = this.state;
const field = !offer ? 'offer' : 'data';
setTimeout(() => {
alert('setTimeout started');
this.setState({
[field]: JSON.stringify(pc.localDescription)
});
}, 2000);
}
};
}
@autobind
setConnectionState() {
this.setState({
connectionState: this.pc.iceConnectionState
});
}
getUserMedia() {
MediaStreamTrack.getSources(() => {
getUserMedia(
{
audio: false,
video: true
},
this.getUserMediaSuccess,
this.getUserMediaError
);
});
}
@autobind
async getUserMediaSuccess(stream) {
const { pc } = this;
pc.addStream(stream);
await this.setState({ videoURL: stream.toURL() });
if (this.state.initiator) {
return this.createOffer();
}
return this.createAnswer();
}
getUserMediaError(error) {
console.log(error);
}
@autobind
logError(error) {
const errorArray = [...this.state.error, error];
return this.setState({
error: errorArray
});
}
/**
* Create offer
*
* @memberof HomeScreen
*/
@autobind
createOffer() {
const { pc } = this;
pc.createOffer()
.then(offer => pc.setLocalDescription(offer))
.then(() => {
this.setState({
offerCreated: true
});
})
.catch(this.logError);
}
/**
* Create anwser
*
* @memberof HomeScreen
*/
@autobind
async createAnswer() {
const { pc } = this;
const { data } = this.state;
if (data) {
const sd = new RTCSessionDescription(JSON.parse(data));
await this.setState({
offerImported: true
});
pc.setRemoteDescription(sd)
.then(() => pc.createAnswer())
.then(answer => pc.setLocalDescription(answer))
.then(() => {
this.setState({
answerCreated: true
});
})
.catch(this.logError);
}
}
@autobind
receiveAnswer() {
const { pc } = this;
const { data } = this.state;
const sd = new RTCSessionDescription(JSON.parse(data));
return pc
.setRemoteDescription(sd)
.then(() => {
this.setState({
answerImported: true
});
})
.catch(this.logError);
}
/**
* Start communication
*
* @param {boolean} [initiator=true]
* @returns
* @memberof HomeScreen
*/
@autobind
async start(initiator = true) {
if (!initiator) {
await this.setState({
initiator: false
});
}
return this.getUserMedia();
}
Anyone can help me?
Upvotes: 1
Views: 1929
Reputation: 42450
No iceServers
is fine on a LAN, but peers must still exchange at least one candidate: their host candidate (based on their machine's LAN IP address).
Either:
Trickle candidates using onicecandidate
-> signaling -> addIceCandidate
as usual, or
Out-wait the ICE process (a few seconds) before exchanging pc.localDescription
.
It looks like you're attempting the latter. This approach works because...
The signaling (trickling) of individual ice candidates using onicecandidate
, is an optimization meant to speed up negotiation. Once setLocalDescription
succeeds, the browser's internal ICE Agent starts, inserting ICE candidates, as they're discovered, into the localDescription
itself. Wait a few seconds to negotiate, and trickling isn't necessary at all: all ICE candidates will be in the offer and answer transmitted.
From your onicecandidate
code it looks like you're already attempting to gather the localDescription
after ICE completion (and you've written it to work from both ends):
pc.onicecandidate = ({ candidate }) => {
if (!candidate) {
const { offer } = this.state;
const field = !offer ? 'offer' : 'data';
this.setState({
[field]: JSON.stringify(pc.localDescription)
});
}
};
In the offerer side you've correctly commented out the equivalent code in createOffer
:
pc.createOffer()
.then(offer => pc.setLocalDescription(offer))
.catch(this.logError);
// .then(() => {
// this.setState({
// offer: JSON.stringify(pc.localDescription)
// });
// });
But on the answerer side, you have not, and that's likely the problem:
createAnswer() {
const { pc } = this;
const { data } = this.state;
if (data) {
const sd = new RTCSessionDescription(JSON.parse(data));
pc.setRemoteDescription(sd)
.then(() => pc.createAnswer())
.then(answer => pc.setLocalDescription(answer))
.then(() => {
this.setState({
offer: JSON.stringify(pc.localDescription)
});
})
.catch(this.logError);
}
}
This means it sends an answer back immediately, before the answerer's ICE agent has had time to insert any candidates into the answer. This is probably why it fails.
On a side-note: Nothing appears to wait for getUserMedia
to finish either, so answers likely won't contain any video either, depending on the timing of your getUserMediaSuccess
function, which fails to add any tracks or streams to the connection. But assuming you're just doing data channels, this should work with my recommended fixes.
Upvotes: 2