目标:
- 拨打视频电话
- 同意视频电话
- 连接出现自己和对方的画面
- 单方面挂断电话,另一方会被挂断
- 基于信令服务器通信,详情见:webrtc进阶实战之信令服务器 - 简书 (jianshu.com)
init
call
等待同意
对方同意
发送文本消息
一些消息
已挂断
信令服务器打印
视频电话demo代码:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div class="rows">
<div class="box">
<video id="other-video"></video>
<video id="my-video"></video>
</div>
<div id="message">
<div class="title">消息</div>
</div>
</div>
<div class="row">
<input placeholder="请输入..." style="width: 380px; padding: 4px 10px;" id="text" />
<button id="sendText">发送消息</button>
</div>
<button id="close" style="display: none; margin-top: 10px;">挂断</button>
<button id="answer" style="display: none; margin-top: 10px;">calling...同意</button>
<br>
<div>在线人员:</div>
<div id="list"></div>
</body>
<script>
const answerButton = document.getElementById('answer');
const closeButton = document.getElementById('close');
const otherVideo = document.getElementById('other-video');
const myVideo = document.getElementById('my-video');
const listDiv = document.getElementById('list');
const name = 'ws11221122'// + (Math.random() * 1000).toFixed(0);
console.log(name, 'name')
let rtc, hasOffer, hasRomve, list, targetName;
const err = (e) => {
console.log(e);
}
const send = (data) => {
ws.send(JSON.stringify({ ...data, name, targetName }));
}
const getVideo = async (callback) => {
const gumStream = await navigator.mediaDevices.getUserMedia(
{ video: true, audio: true });
for (const track of gumStream.getTracks()) {
rtc.addTrack(track, gumStream);
}
rtc.addStream(gumStream);
myVideo.srcObject = gumStream;
myVideo.onloadedmetadata = () => {
myVideo.play();
}
callback();
}
const closeHandler = () => {
if (rtc) {
otherVideo.srcObject && otherVideo.srcObject.getTracks().forEach(v => {
v.stop();
});
otherVideo.srcObject = null;
myVideo.srcObject && myVideo.srcObject.getTracks().forEach(v => {
v.stop();
});
myVideo.srcObject = null;
// rtc.close();
}
}
const ws = new WebSocket('ws://localhost:3002', name);
ws.onopen = (evt) => {
console.log('ws open ...');
ws.send(JSON.stringify({ name, type: 'init', targetName }));
};
ws.onmessage = (evt) => {
let res = {};
try {
res = JSON.parse(evt.data);
} catch { }
console.log(res.type);
if (res.type === 'offer') {
answerButton.style.display = 'block';
hasOffer = res.offer;
targetName = res.name;
} else if (res.type === 'answer') {
rtc.setRemoteDescription(new RTCSessionDescription(res.answer), () => {
console.log('收到answer setRemoteDescription ok');
hasRomve = true;
})
} else if (rtc && res.type === 'ice') {
if (rtc && res.candidate && hasRomve) {
rtc.addIceCandidate(new RTCIceCandidate(res.candidate))
}
} else if (res.type === 'list') {
list = res.list;
listDiv.innerHTML = res.list.sort().map(i => '<p>' + i + (i !== name ? `<button class='button' onclick='callHandler("${i}")' id='${i}' style="display: block;">call</button>` : '(本人)') + '</p>').join('')
} else if (rtc && res.type === 'close') {
closeHandler()
}
};
ws.onclose = function (evt) {
console.log('Connection closed.');
};
const createOffer = () => {
rtc.createOffer((offer) => {
rtc.setLocalDescription(new RTCSessionDescription(offer), () => {
send({ type: 'offer', offer })
}, err)
}, err, {
offerToReceiveVideo: 1,
})
}
const createAnswer = () => {
rtc.setRemoteDescription(new RTCSessionDescription(hasOffer), () => {
hasRomve = true;
rtc.createAnswer((answer) => {
rtc.setLocalDescription(answer);
send({ type: 'answer', answer })
}, err);
}, err)
}
const createRTC = (callback) => {
rtc = new RTCPeerConnection();
getVideo(callback)
channel = rtc.createDataChannel("sendChannel");
channel.onopen = () => {
console.log('channel onopen')
};
channel.onclose = () => {
console.log('channel onclose')
};
rtc.ontrack = (s) => {
console.log('ontrack')
otherVideo.srcObject = s.streams[0];
otherVideo.onloadedmetadata = () => {
otherVideo.play();
closeButton.style.display = 'block';
}
}
rtc.ondatachannel = (e) => {
console.log('ondatachannel')
e.channel.onmessage = (ev) => {
const message = ev.data;
const d = document.createElement('p');
d.innerText = message;
document.getElementById('message').appendChild(d)
}
document.getElementById('sendText').onclick = () => {
const value = document.getElementById('text').value;
channel.send(value);
}
}
rtc.onicecandidate = (ice) => {
console.log('onicecandidate')
if (ice && ice.candidate) {
send({ type: 'ice', candidate: ice.candidate })
}
}
}
answerButton.onclick = () => {
createRTC(createAnswer);
answerButton.style.display = 'none';
};
const callHandler = (id) => {
targetName = id;
createRTC(createOffer);
// callButton = document.getElementById(id);
// callButton.innerText = 'calling'
};
closeButton.onclick = () => {
closeHandler();
send({ type: 'close' })
}
</script>
<style>
.box {
position: relative;
width: 500px;
height: 400px;
}
#my-video {
border: 1px solid #999;
width: 200px;
height: 160px;
position: absolute;
bottom: 4px;
right: 4px;
background: #999;
}
#other-video {
border: 1px solid #999;
width: 500px;
height: 400px;
background: #eee;
}
p {
display: flex;
flex-direction: row;
}
p button {
margin-left: 10px;
}
button {
padding: 4px 12px;
border: 0;
background: rgb(14, 81, 251);
color: #fff;
border-radius: 4px;
font-size: 14px;
}
.rows {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.row {
margin-top: 10px;
width: 500px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
#message {
float: right;
margin-right: 20px;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 10px;
border-bottom: 1px solid #999;
width: 300px;
padding-bottom: 4px;
}
</style>
</html>
- 注意 name 需要有且唯一
- 很多状态的监听事件还可以完善,这里就感受一下,写的比较简单
- 代码粘去就可以用,信令服务器代码在上一篇
- 样式比较丑,demo嘛
- 为了黏贴就可以运行使用,没有用任何框架,插件等
- 消息那块写完没有把自己发送的内容协商,然后一左一右 可能会比较真实感,不想改了,年底了,新年快乐。
网友评论