I can't connect via webRTC, I get /connectionstate=failed - webrtc

On webrtc debugger I have this from pc1 (call start):
From pc2 (join call):
Have error:
url: stun:54.166.135.129%C2%A0:443
address: 192.168.1.x
port: 59351
host_candidate: 192.168.1.x:59351
error_text: STUN host lookup received error.
error_code: 701
This is code for START CALL:
const startCall = async () => {
sendUser();
localStream = await navigator.mediaDevices.getUserMedia({
video: {
frameRate: 7,
width: 320,
height: 240,
aspectRatio: 1.33333,
},
audio: true,
});
localVideoRef!.current!.srcObject = localStream;
let configuration = {
iceServers: [
{ urls: 'stun:54.166.135.129:443' },
{ urls: 'turn:54.166.135.129 :443', credential: 'user', username: '123' },
],
};
peerConn = new RTCPeerConnection(configuration);
remoteStream = new MediaStream();
// Push tracks from local stream to peer connection
localStream.getTracks().forEach(function (track) {
peerConn.addTrack(track, localStream);
});
// Pull tracks from remote stream, add to video stream
peerConn.ontrack = (event) => {
event.streams[0].getTracks().forEach((track, index) => {
console.log(`REMOTE TRACK ${index + 1}: `, track);
remoteStream.addTrack(track);
});
console.log(remoteStream);
};
peerConn.onicecandidateerror = function (error) {
console.error('On ICE Candidate Error:', error);
};
peerConn.onsignalingstatechange = function (event) {
console.log('Signaling state change:', peerConn.signalingState);
};
peerConn.oniceconnectionstatechange = function (event) {
console.log('ICE connection state change:', peerConn.iceConnectionState);
if (peerConn.iceConnectionState === 'failed') {
console.error('ICE connection failed');
}
};
remoteVideoRef!.current!.srcObject = remoteStream;
peerConn.onicecandidate = (e) => {
if (e.candidate == null) return;
sendData({
type: 'store_candidate',
candidate: e.candidate,
});
};
await createAndSendOffer();
};
const createAndSendOffer = async () => {
const offer = await peerConn.createOffer();
await peerConn.setLocalDescription(offer);
sendData({
type: 'store_offer',
offer: offer,
});}
function sendData(data: { type?: string; candidate?: RTCIceCandidate; offer?: any; id?: string }) {
data.id = id;
console.log('SEND DATA: ', data);
webSocket.send(JSON.stringify(data));
}
function handleSignallingData(data: {
type: any;
answer: RTCSessionDescriptionInit;
offer: RTCSessionDescriptionInit;
candidate: RTCIceCandidateInit | RTCIceCandidate;
}) {
switch (data.type) {
case 'answer':
console.log('ANSWER:', data.answer);
peerConn.setRemoteDescription(new RTCSessionDescription(data.answer));
break;
case 'offer':
console.log('OFFER:', data.offer);
peerConn.setRemoteDescription(data.offer);
createAndSendAnswer();
break;
case 'candidate':
peerConn.addIceCandidate(new RTCIceCandidate(data.candidate));
console.log('CANDIDATE:', data.candidate);
}
}
This is code for JOIN CALL:
webSocket.onmessage = (event) => {
handleSignallingData(JSON.parse(event.data));
};
function handleSignallingData(data: {
type: any;
answer: RTCSessionDescriptionInit;
offer: RTCSessionDescriptionInit;
candidate: RTCIceCandidateInit | RTCIceCandidate;
}) {
switch (data.type) {
case 'answer':
console.log('ANSWER:', data.answer);
peerConn.setRemoteDescription(new RTCSessionDescription(data.answer));
break;
case 'offer':
console.log('OFFER:', data.offer);
peerConn.setRemoteDescription(new RTCSessionDescription(data.offer));
createAndSendAnswer();
break;
case 'candidate':
peerConn.addIceCandidate(new RTCIceCandidate(data.candidate));
console.log('CANDIDATE:', data.candidate);
}
}
function sendData(data: { type?: string; candidate?: RTCIceCandidate; answer?: any; id?: any }) {
data.id = chatUserId;
console.log('SEND DATA: ', data);
webSocket.send(JSON.stringify(data));
}
const joinCall = async (stop?: boolean) => {
localStream = await navigator.mediaDevices.getUserMedia({
video: {
frameRate: 10,
width: 320,
height: 240,
aspectRatio: 1.33333,
},
audio: true,
});
// Let's check if we have current. I have few times exception here
if (!localVideoRef!.current) return;
localVideoRef!.current!.srcObject = localStream;
let configuration = {
iceServers: [
{ urls: 'stun:54.166.135.129:443' },
{ urls: 'turn:54.166.135.129 :443', credential: 'user', username: '123' },
],
};
peerConn = new RTCPeerConnection(configuration);
remoteStream = new MediaStream();
// Push tracks from local stream to peer connection
peerConn.ontrack = (event) => {
event.streams[0].getTracks().forEach((track, index) => {
console.log(`REMOTE TRACK ${index + 1}: `, track);
remoteStream.addTrack(track);
});
console.log(remoteStream);
};
peerConn.onicecandidateerror = function (error) {
console.error('On ICE Candidate Error:', error);
};
peerConn.onsignalingstatechange = function (event) {
console.log('Signaling state change:', peerConn.signalingState);
};
peerConn.oniceconnectionstatechange = function (event) {
console.log('ICE connection state change:', peerConn.iceConnectionState);
if (peerConn.iceConnectionState === 'failed') {
console.error('ICE connection failed');
}
};
remoteVideoRef!.current!.srcObject = remoteStream;
peerConn.onicecandidate = (e) => {
if (e.candidate == null) return;
sendData({
type: 'send_candidate',
candidate: e.candidate,
});
};
sendData({
type: 'join_call',
});
};
const createAndSendAnswer = async () => {
const answer = await peerConn.createAnswer();
await peerConn.setLocalDescription(answer);
sendData({
type: 'send_answer',
// #ts-ignore
answer,
});
};
We have a stun and turn server and we don't understand the problem.

Related

WebRTC(simple-peer) doesn't get the signal after I added APIs on server

I am using simple-peer and it really worked well on server before I added APIs for my project. I already did secure with https in the beginning and the only thing that has changed is releasing the server with APIs... Here is my code and now I only can check console.log(1), (3) for initiator peer and console.log(2), (8) for requesting peer. On requestinng peer internet tab, "Uncaught ReferenceError: process is not defined" this error occurs and I don't know why this error occurs on client side. Also it worked well on both of Chrome and Edge but now I can't even get my own stream on Chrome.
const myVideo = useRef();
const userVideo = useRef();
const connectionRef = useRef();
const roomName = "123";
let userStream = null;
let creator = false;
useEffect(() => {
const socket = io("https://www.jg-jg.shop");
socket.emit("joinRoom", roomName);
socket.on("created", () => {
creator = true;
navigator.mediaDevices
.getUserMedia({ video: true, audio: true })
.then((stream) => {
userStream = stream;
myVideo.current.srcObject = stream;
console.log(1);
});
});
socket.on("joined", () => {
navigator.mediaDevices
.getUserMedia({ video: true, audio: true })
.then((stream) => {
userStream = stream;
myVideo.current.srcObject = stream;
console.log(2);
});
socket.emit("ready", roomName);
});
socket.on("ready", () => {
if (creator) {
const peer = new Peer({
initiator: true,
trickle: false,
stream: userStream,
});
peer.on("signal", (signal) => {
socket.emit("sendingSignal", { signal, roomName });
console.log(3);
});
peer.on("stream", (stream) => {
userVideo.current.srcObject = stream;
console.log(4);
});
socket.on("receivingSignal", (signal) => {
peer.signal(signal);
console.log(5);
});
connectionRef.current = peer;
}
});
socket.on("offer", (incomingSignal) => {
if (!creator) {
const peer = new Peer({
initiator: false,
trickle: false,
stream: userStream,
});
peer.on("signal", (signal) => {
socket.emit("returningSignal", { signal, roomName });
console.log(6);
});
peer.on("stream", (stream) => {
userVideo.current.srcObject = stream;
console.log(7);
});
peer.signal(incomingSignal);
console.log(8);
connectionRef.current = peer;
}
});
}, []);

Can't get WebRTC to work in different networks

I am trying to transfer a video streaming from one browser to another with WebRTC and socket.io. It works just fine in the same network. No image is getting through across different ones.
I use socket-io as a signal server. I register two browsers in a "room" and then start sending signals.
The code which is executed in the browser from which the streaming is sent:
function joinRoom(room) {
if (room === '') {
alert('Please type a room ID')
} else {
data = { room: room};
socket.emit('join', data);
}
}
// SOCKET EVENT CALLBACKS =====================================================
socket.on('room_created', async () => {
console.log('Socket event callback: room_created')
await setLocalStream(mediaConstraints)
socket.emit('startc', {room: roomId, clientip: clientip})
isRoomCreator = true
})
socket.on('full_room', () => {
console.log('Socket event callback: full_room')
alert('The room is full, please try another one')
})
socket.on('startc', async () => {
console.log('Socket event callback: start_call')
if (isRoomCreator) {
rtcPeerConnection = new RTCPeerConnection(iceServers)
addLocalTracks(rtcPeerConnection)
rtcPeerConnection.ontrack = setRemoteStream
rtcPeerConnection.onicecandidate = sendIceCandidate
await createOffer(rtcPeerConnection)
}
})
socket.on('offer', async (event) => {
console.log('Socket event callback: offer')
if (!isRoomCreator) {
rtcPeerConnection = new RTCPeerConnection(iceServers)
addLocalTracks(rtcPeerConnection)
rtcPeerConnection.ontrack = setRemoteStream
rtcPeerConnection.onicecandidate = sendIceCandidate
rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(event))
await createAnswer(rtcPeerConnection)
}
})
socket.on('answer', (event) => {
console.log('answer');
console.log('Socket event callback: webrtc_answer')
rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(event))
})
socket.on('webrtc_ice_candidate', (event) => {
console.log('Socket event callback: webrtc_ice_candidate')
// ICE candidate configuration.
var candidate = new RTCIceCandidate({
sdpMLineIndex: event.label,
candidate: event.candidate,
})
rtcPeerConnection.addIceCandidate(candidate)
})
The code that accepts the streamed media (vuejs):
socket.on("offer", (data) => {
this.$emit("closeWaitingToConnect");
this.createAnswer(data);
});
joinMeToRoom() {
console.log("joinToRoom: ", this.room);
this.$socket.emit("join", this.room);
}, //joinMeToRoom ()
createAnswer: function(event) {
var roomId = this.room.room;
let sessionDescription
this.peer.ontrack = this.setRemoteStream
this.peer.onicecandidate = this.sendIceCandidate
this.peer.setRemoteDescription(new RTCSessionDescription(event))
try {
sessionDescription = this.peer.createAnswer().then((answer) => {
var anwer =
console.log('sessionDescription');
console.log(answer);
this.$socket.emit('answer', {
type: 'webrtc_answer',
sdp: answer,
sessionDescription: JSON.stringify(answer),
roomId,
})
return this.peer.setLocalDescription(answer)
});
} catch (error) {
console.error('cae: '+error)
}
},
getScreenPosition() {
const right = this.$refs.screen.getBoundingClientRect().right;
const bottom = this.$refs.screen.getBoundingClientRect().bottom;
return { bottom: bottom, right: right };
},
setRemoteStream(event) {
console.log('event setRemoteStream');
console.log(event);
var stream_screen = document.querySelector("video");
stream_screen.srcObject = event.streams[0];
stream_screen.play();
var remoteStream = event.stream
},
I have setup my own TURN server and tried paid versions. Still can't get the stream across different networks.
What am I missing?

How to share screen using webRTC

I need to get the screen sharing working. It is working if it is video sharing. This is the code:
public n = navigator as any;
ngAfterViewInit(): void {
const video = this.myVideo.nativeElement;
let peerx: any;
this.n.getUserMedia =
this.n.getUserMedia ||
this.n.webkitGetUserMedia ||
this.n.mozGetUserMedia ||
this.n.msGetUserMedia;
}
this.n.getUserMedia( // this.n.mediaDevices.getDisplayMedia
{
video: {
madatory: {
chromeMediaSource: 'screen',
maxWidth: 1920,
maxHeight: 1080,
minAspectRatio: 1.77
},
}
},
(stream: any) => {
peerx = new SimplePeer({
initiator: location.hash === '#init',
trickle: false,
stream,
});
peerx.on('signal', (data) => {
const strData = JSON.stringify(data);
console.log(strData);
this.targetpeer = data;
});
peerx.on('stream', (streamX: any) => {
if ('srcObject' in video) {
video.srcObject = streamX;
} else {
video.src = window.URL.createObjectURL(streamX);
}
const playPromise = video.play();
if (playPromise !== undefined) {
playPromise
.then((_) => {
video.play();
})
.catch((error) => {
console.log(`Playing was prevented: ${error}`);
});
}
});
If I change the line 'this.n.getUserMedia(....)' to this 'this.n.mediaDevices.getDisplayMedia(...)', I do not get the 'signal' (the key that I need to paste on the client to connect).
You are attempting to mix a constraints style that was needed years ago when a Chrome extension was required with getDisplayMedia. That isn't going to work.
const stream = await navigator.mediaDevices.getDisplayMedia({video: true})
See here for a canonical sample.

Uncaught (in promise) DOMException: Error processing ICE candidate WebRTC RTCIceCandidate

turns out this error please help where the problem
** Uncaught (in promise) DOMException: Error processing ICE candidate **
here is the source code of the call
ConOut.getStartScreenSharing => ConIn.socket.on('OnMessage') => onOffer()
there the getStartScreenSharing function is called
did everything according to the example
using Chrome 68.0.3440.106 browser
import Vue from "vue";
export default class ConOut {
constructor() {
let t = this;
t.pc = new RTCPeerConnection();
this.socket = Vue.prototype.$signalR;
this.socket.on('OnMessage', (chName, message) => {
var desc = JSON.parse(message);
if (desc.type === 'candidate') {
t.pc.addIceCandidate(new RTCIceCandidate({
sdpMLineIndex: desc.label,
candidate: desc.candidate
}))
}
if (desc.type == 'answer') {
t.pc.setRemoteDescription(desc);
}
});
this.socket.start().then(function () {});
}
send = function (message) {
this.socket.invoke("SendOtherV2", this.channelName, JSON.stringify(message));
}
getStartScreenSharing() {
let t = this;
navigator.getUserMedia({
audio: false,
video: true
}, function (stream) {
t.pc.addStream(stream);
t.pc.createOffer().then(function (ff) {
t.pc.setLocalDescription(ff);
t.send({
type: "offer",
offer: ff
});
});
t.pc.onicecandidate = function (event) {
if (event.candidate) {
t.send({
type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
})
};
t.pc.onaddstream = function (s) {
t.CreateVideoTag(s)
}
};
})
}
};
here we wait for the Offer and answer
import Vue from "vue";
export default class ConIn {
constructor() {
let t = this;
t.pc = new RTCPeerConnection();
this.socket = Vue.prototype.$signalR;
this.socket.on('OnMessage', (chName, message) => {
var desc = JSON.parse(message);
if (desc.type === 'candidate') {
console.log(desc)
t.pc.addIceCandidate(new RTCIceCandidate({
sdpMLineIndex: desc.label,
candidate: desc.candidate
}))
} else {
t.onOffer(desc.offer)
}
});
this.socket.start().then(function () {});
}
send = function (message) {
this.socket.invoke("SendOtherV2", this.channelName, JSON.stringify(message));
}
onOffer(offer) {
let t = this;
navigator.getUserMedia({
audio: false,
video: true
}, function (stream) {
t.pc.addStream(stream);
t.pc.setRemoteDescription(new RTCSessionDescription(offer), function () {
t.pc.createAnswer(function (answer) {
t.pc.setLocalDescription(answer);
t.send(answer)
});
});
t.pc.onicecandidate = function (event) {
if (event.candidate) {
t.send({
type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
})
}
t.pc.onaddstream = function (s) {
t.CreateVideoTag(s)
}
};
})
}
};
here is the code that works without errors
ConOut.js
import Vue from "vue";
export default class ConOut {
constructor() {
let t = this;
t.pc = new RTCPeerConnection();
t.pc.onicecandidate = function (event) {
if (event.candidate) {
t.send({
type: 'candidate',
candidate: event.candidate
})
};
};
t.pc.onaddstream = function (s) {
console.log('onaddstream')
t.createVideoTag(s)
}
this.socket = Vue.prototype.$signalR;
this.socket.on('OnMessage', (chName, message) => {
var desc = JSON.parse(message);
if (desc.type === 'candidate') {
t.pc.addIceCandidate(new RTCIceCandidate(desc.candidate))
}
if (desc.type == 'answer') {
t.pc.setRemoteDescription(new RTCSessionDescription(desc), function () {
console.log('Setting remote description by answer');
}, function (e) {
console.error(e);
});
}
});
this.socket.start().then(function () {
console.info('SignalR connection is opened.');
});
}
send = function (message) {
this.socket.invoke("SendOtherV2", this.channelName, JSON.stringify(message));
}
getStartScreenSharing() {
let t = this;
navigator.getUserMedia({
audio: false,
video: true
}, function (stream) {
t.pc.addStream(stream);
t.pc.createOffer().then(function (ff) {
t.pc.setLocalDescription(ff);
t.send({
type: "offer",
offer: ff
});
});
})
}
createVideoTag = function (s, isRemote = false) {
let videoContener = document.getElementById('videoContener');
let x = document.createElement("VIDEO");
x.setAttribute("width", "320");
x.setAttribute("height", "240");
x.srcObject = s.stream;
x.controls = true
x.playsinline = true
x.autoplay = true
videoContener.appendChild(x);
return x;
}
};
ConIn.js
import Vue from "vue";
export default class ConIn {
constructor() {
let t = this;
t.ic = [];
t.isSetRD = false
t.pc = new RTCPeerConnection();
t.pc.onicecandidate = function (event) {
if (event.candidate) {
t.send({
type: 'candidate',
candidate: event.candidate
})
};
};
t.pc.onaddstream = function (s) {
console.log('onaddstream')
t.createVideoTag(s)
}
this.socket = Vue.prototype.$signalR;
this.socket.on('OnMessage', (chName, message) => {
var desc = JSON.parse(message);
console.log(desc)
if (desc.type === 'candidate') {
setTimeout(function () {
t.pc.addIceCandidate(new RTCIceCandidate(desc.candidate))
}, 5000);
} else {
t.onOffer(desc.offer)
}
});
this.socket.start().then(function () {
if (t.enableLogs) {
console.info('SignalR connection is opened.');
}
});
}
send = function (message) {
this.socket.invoke("SendOtherV2", this.channelName, JSON.stringify(message));
}
onOffer(offer) {
let t = this;
navigator.getUserMedia({
audio: false,
video: true
}, function (stream) {
t.pc.addStream(stream);
t.pc.setRemoteDescription(new RTCSessionDescription(offer))
.then(() => t.pc.createAnswer())
.then(answer => {
t.pc.setLocalDescription(answer)
t.send(answer)
})
.catch(e => console.error(e));
})
}
createVideoTag = function (s, isRemote = false) {
let videoContener = document.getElementById('videoContener');
let x = document.createElement("VIDEO");
x.setAttribute("width", "320");
x.setAttribute("height", "240");
x.srcObject = s.stream;
x.controls = true
x.playsinline = true
x.autoplay = true
videoContener.appendChild(x);
return x;
}
};

webrtc each step success but no video share

chrome version: 62.0.3202.94;
firefox version: 57.0.1;
I write a simple demo use webrtc and socket.io.
It works with pages. For example, I open one page to connect socket, and waiting for PeerConnection info from the main page(which get the local media). When I open the main, I create ice and sdp and then exchange them by socket.io to create connection.
Here is the code.
// The server side:
const express = require('express')
const app = express()
const path = require('path')
app.use(express.static(path.join(__dirname, 'public')))
app.get('/phone', function(req, res) {
res.sendfile(__dirname + '/phone.html')
})
app.get('/', function(req, res) {
res.sendfile(__dirname + '/index.html')
})
const server = require('http').createServer(app)
const io = require('socket.io')(server)
let clients = []
io.on('connection', function(socket) {
clients.push(socket)
const referer = socket.handshake.headers.referer
// socket connect from '/phone'
if (referer.match('/phone')) {
// send the ice from phone to others
socket.on('phone_ice_candidate', function(res) {
socket.broadcast.emit('pc_add_ice', {
ice: res.ice
})
})
// send the sdp from phone to others
socket.on('send_phone_sdp', function(data) {
socket.broadcast.emit('set_pc_remote_sdp', {
desc: data.desc
})
})
}
// phone add ice from web
socket.on('remote_ice_candidate', function(ice) {
socket.to(getId(clients, '/phone')).emit('send_ice_to_pc', {
ice: ice
})
})
// phone add sdp from web
socket.on('send_pc_sdp', function(data) {
// send to phone
socket.to(getId(clients, '/phone')).emit('set_phone_remote_sdp', {
desc: data
})
})
// socket disconnect and remove it from clients
socket.on('disconnect', () => {
let id = socket.id
clients.forEach((client, index) => {
if (client.id === id) {
clients.splice(index, 1)
}
})
})
})
// get the socket id to emit
function getId(sockets, exp) {
let id
sockets.forEach(socket => {
if (socket.handshake.headers.referer.match(exp)) {
id = socket.id
}
})
return id
}
server.listen(3000, function() {
console.log('port listening at 3000')
})
// --------------------------------------------- //
// web.js
var socket = io();
var server = {
// "iceServers": [{
// "url": "stun:stun.l.google.com:19302"
// }]
},
pc = new RTCPeerConnection(null),
v = document.querySelector('#video2')
// web onicecandidate
pc.onicecandidate = function(event) {
if (event.candidate) {
socket.emit('remote_ice_candidate', {
ice: event.candidate
})
}
}
// web addIceCandidate
socket.on('pc_add_ice', function(event) {
pc.addIceCandidate(new RTCIceCandidate(event.ice))
})
// didn't trigger
pc.ontrack = function(e) {
// v.srcObject = e.streams[0];
console.log(e, 'pc.ontrack')
}
// web setRemoteDescription and createAnswer
socket.on('set_pc_remote_sdp', function(e) {
pc.setRemoteDescription(e.desc).then(
function() {
console.log('remote setRemoteDescription success')
pc.createAnswer().then(function(desc) {
pc.setLocalDescription(desc).then(
function() {
socket.emit('send_pc_sdp', {
desc: desc
})
},
function(err) {
console.log(err)
}
);
})
},
function() {
console.log('pc setLocalDescription error')
}
)
})
// web iceConnectionState
pc.oniceconnectionstatechange = function() {
console.log('web oniceconnectionstatechange', pc.iceConnectionState)
// log checking -> connected
};
//---------------------------------------------//
// phone.js
var socket = io();
var server = {
// "iceServers": [{
// "url": "stun:stun.l.google.com:19302"
// }]
},
pc = new RTCPeerConnection(null),
v = document.querySelector('#video1')
// phone onicecandidate
pc.onicecandidate = function(event) {
if (event.candidate) {
socket.emit('phone_ice_candidate', {
ice: event.candidate
})
}
}
// phone addIceCandidate
socket.on('send_ice_to_pc', function(event) {
pc.addIceCandidate(new RTCIceCandidate(event.ice.ice))
})
// getUserMedia
navigator.mediaDevices.getUserMedia({
video: {
width: 400,
height: 300
},
audio: false
})
.then(function(stream) {
v.src = window.URL.createObjectURL(stream);
pc.addStream(stream);
})
.then(function() {
// create offer
pc.createOffer({
offerToReceiveVideo: 1
}).then(function(e) {
// pc setLocalDescription
pc.setLocalDescription(e).then(
function() {
socket.emit('send_phone_sdp', {
desc: e
})
},
function() {
console.log('pc setLocalDescription error')
}
)
});
})
.catch(function(err) {
console.log(err.name + ": " + err.message);
})
// phone setRemoteDescription
socket.on('set_phone_remote_sdp', function(e) {
pc.setRemoteDescription(e.desc.desc).then(
function() {
console.log('pc setRemoteDescription success')
},
function(err) {
console.log(err)
})
})
// phone iceConnectionState
pc.oniceconnectionstatechange = function() {
console.log('phone oniceconnectionstatechange', pc.iceConnectionState)
// log checking -> connected -> completed
};
When i use firefox to open it, there is an error ICE failed, add a STUN server and see about:webrtc for more details in console.
In chrome the 'phone iceConnectionState' changed checking -> connected -> completed, the 'web iceConnectionState' changed checking -> connected.
have you set autoplay in your html? I have the same issue, and it turns out I should have set autoplay in my html tag. Namely:
<video autoplay></video>
Hope this helps!
getUserMedia is an async function. You are calling createOffer before you call pc.addStream which means there is nothing to negotiate.
Make the promise callback return your pc.createOffer() after pc.addStream(stream);
PTAL at https://www.html5rocks.com/en/tutorials/webrtc/basics/#toc-signaling and compare your code to the example and see if you can figure it out.