webRTC demo
准备:
- 信令服务
- 前端页面用于视频通话
demo github 地址。
前端页面
为了使 demo 尽量简单,功能页面如下,即包含登录、通过对方手机号拨打电话的功能。在实际生成过程中,未必使用的手机号,可能是任何能代表用户身份的字符串。
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="margin: 20px">
<label for="loginAccount">登录账号</label><input id="loginAccount" name="loginAccount" placeholder="请输入手机号"
type="text">
<button id="login" onclick="login()" type="button">登录</button>
</div>
<div style="margin: 20px">
<video autoplay controls height="360px" id="localVideo" width="640px"></video>
<video autoplay controls height="360px" id="remoteVideo" width="640px"></video>
</div>
<div style="margin: 20px">
<label for="toAccount">对方账号</label>
<input id="toAccount" name="toAccount" placeholder="请输入对方手机号" type="text">
<button id="requestVideo" onclick="requestVideo()" type="button">请求视频通话</button>
</div>
<div style="margin: 20px">
<fieldset>
<button id="accept" type="button">接通</button>
<button id="hangup" type="button">挂断</button>
</fieldset>
</div>
<div style="margin: 20px">
<fieldset>
<div>
录制格式: <select disabled id="codecPreferences"></select>
</div>
<button id="startRecord" onclick="startRecording()" type="button">开始录制视频</button>
<button id="stopRecord" onclick="stopRecording()" type="button">停止录制视频</button>
<button id="downloadRecord" onclick="download()" type="button">下载</button>
</fieldset>
</div>
</body>
<script>
let config = {
iceServers: [
{
'urls': 'turn:turn.wildfirechat.cn:3478',
'credential': 'wfchat',
'username': 'wfchat'
}
]
}
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const requestVideoButton = document.getElementById('requestVideo');
const acceptButton = document.getElementById('accept');
const hangupButton = document.getElementById('hangup');
const codecPreferences = document.querySelector('#codecPreferences');
const recordButton = document.getElementById('startRecord')
const stopRecordButton = document.getElementById('stopRecord')
const downloadButton = document.getElementById('downloadRecord')
const wsAddress = 'ws://localhost:9113/ws';
let loginAttemptCount = 0;
let myId, toId;
let pc, localStream, ws;
let mediaRecorder;
let recordedBlobs;
function login() {
loginAttemptCount = 0;
myId = document.getElementById('loginAccount').value;
ws = new WebSocket(wsAddress);
ws.onopen = function () {
console.log("WebSocket is open now.");
connect();
alert("登录成功");
};
ws.onmessage = function (message) {
let msg = JSON.parse(message.data);
console.log("ws 收到消息:" + msg.type);
switch (msg.type) {
case "offline": {
if (loginAttemptCount < 10) {
setTimeout(() => {
loginAttemptCount++;
watch();
}, 1000);
}
break;
}
case "watch": {
handleWatch(msg);
break;
}
case "offer": {
handleOffer(msg);
break;
}
case "answer": {
handleAnswer(msg);
break;
}
case "candidate": {
handleCandidate(msg);
break;
}
case "hangup": {
handleHangup(msg);
break;
}
}
};
}
requestVideoButton.onclick = async () => {
toId = document.getElementById('toAccount').value;
if (!myId) {
alert('请先登录');
return;
}
if (!toId) {
alert('请输入对方手机号');
return;
}
watch();
localStream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
localVideo.srcObject = localStream;
createPeerConnection();
}
function connect() {
send({
type: "connect",
from: myId
});
}
function handleWatch(msg) {
toId = msg.from;
}
acceptButton.onclick = async () => {
localStream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
localVideo.srcObject = localStream;
createPeerConnection();
pc.createOffer().then(offer => {
pc.setLocalDescription(offer);
send({
type: 'offer',
from: myId,
to: toId,
data: offer
});
});
}
function handleOffer(msg) {
pc.setRemoteDescription(msg.data);
pc.createAnswer().then(answer => {
pc.setLocalDescription(answer);
send({
type: "answer",
from: myId,
to: toId,
data: answer
});
});
}
function watch() {
send({
type: 'watch',
from: myId,
to: toId
});
}
function handleAnswer(msg) {
if (!pc) {
console.error('no peer connection');
return;
}
pc.setRemoteDescription(msg.data);
}
function handleCandidate(msg) {
if (!pc) {
console.error('no peer connection');
return;
}
pc.addIceCandidate(new RTCIceCandidate(msg.data)).then(() => {
console.log('candidate添加成功')
}).catch(handleError)
}
function handleError(error) {
console.log(error);
}
function createPeerConnection() {
pc = new RTCPeerConnection(config);
pc.onicecandidate = e => {
if (e.candidate) {
send({
type: "candidate",
from: myId,
to: toId,
data: e.candidate
});
}
};
pc.ontrack = e => remoteVideo.srcObject = e.streams[0];
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
}
hangupButton.onclick = async () => {
if (pc) {
pc.close();
pc = null;
}
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
localStream = null;
}
send({
type: "hangup",
from: myId,
to: toId
});
}
function handleHangup() {
if (!pc) {
console.error('no peer connection');
return;
}
pc.close();
pc = null;
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
localStream = null;
}
console.log('hangup');
}
function send(msg) {
ws.send(JSON.stringify(msg));
}
function getSupportedMimeTypes() {
const possibleTypes = [
'video/webm;codecs=vp9,opus',
'video/webm;codecs=vp8,opus',
'video/webm;codecs=h264,opus',
'video/mp4;codecs=h264,aac',
];
return possibleTypes.filter(mimeType => {
return MediaRecorder.isTypeSupported(mimeType);
});
}
function startRecording() {
recordedBlobs = [];
getSupportedMimeTypes().forEach(mimeType => {
const option = document.createElement('option');
option.value = mimeType;
option.innerText = option.value;
codecPreferences.appendChild(option);
});
const mimeType = codecPreferences.options[codecPreferences.selectedIndex].value;
const options = {mimeType};
try {
mediaRecorder = new MediaRecorder(remoteVideo.srcObject, options);
} catch (e) {
console.error('Exception while creating MediaRecorder:', e);
alert('Exception while creating MediaRecorder: ' + e);
return;
}
console.log('Created MediaRecorder', mediaRecorder, 'with options', options);
recordButton.textContent = 'Stop Recording';
mediaRecorder.onstop = (event) => {
console.log('Recorder stopped: ', event);
console.log('Recorded Blobs: ', recordedBlobs);
};
mediaRecorder.ondataavailable = handleDataAvailable;
mediaRecorder.start();
console.log('MediaRecorder started', mediaRecorder);
}
function handleDataAvailable(event) {
console.log('handleDataAvailable', event);
if (event.data && event.data.size > 0) {
recordedBlobs.push(event.data);
}
}
function stopRecording() {
mediaRecorder.stop();
}
function download() {
const blob = new Blob(recordedBlobs, {type: 'video/webm'});
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'test.webm';
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
</script>
</html>
信令服务
基于 JDK 1.8 Spring Boot、Netty 搭建,主要用于解决两个问题:
- 确认参与人,即拨打视频电话的人和接通视频电话的人
- 提供功能按钮 API,比如:发起视频通话、挂电话、以及 webRTC 建立通信通道
主要功能如下:
switch (event.getType()) {
case "connect": {
USER_MAP.put(event.getFrom(), ctx);
break;
}
case "watch": {
WebRtcEvent watchRequest = new WebRtcEvent();
if (USER_MAP.containsKey(event.getTo())) {
watchRequest.setType("watch");
watchRequest.setFrom(event.getFrom());
watchRequest.setTo(event.getTo());
USER_MAP.get(event.getTo()).writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(watchRequest)));
} else {
watchRequest.setType("offline");
USER_MAP.get(event.getFrom()).writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(watchRequest)));
}
break;
}
case "offer": {
WebRtcEvent offerRequest = new WebRtcEvent();
offerRequest.setType("offer");
offerRequest.setFrom(event.getFrom());
offerRequest.setTo(event.getTo());
offerRequest.setData(event.getData());
USER_MAP.get(event.getTo()).writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(offerRequest)));
break;
}
case "answer": {
WebRtcEvent answerRequest = new WebRtcEvent();
answerRequest.setType("answer");
answerRequest.setFrom(event.getFrom());
answerRequest.setData(event.getData());
USER_MAP.get(event.getTo()).writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(answerRequest)));
break;
}
case "candidate": {
WebRtcEvent candidateRequest = new WebRtcEvent();
candidateRequest.setType("candidate");
candidateRequest.setFrom(event.getFrom());
candidateRequest.setData(event.getData());
USER_MAP.get(event.getTo()).writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(candidateRequest)));
break;
}
case "hangup": {
WebRtcEvent hangupRequest = new WebRtcEvent();
hangupRequest.setType("hangup");
hangupRequest.setFrom(event.getFrom());
hangupRequest.setTo(event.getTo());
USER_MAP.get(event.getTo()).writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(hangupRequest)));
break;
}
}
connect -> 登录
与 html 页面中的“登录”按钮对应,当输入手机号后,点击登录,手机号将会在信令服务中存到 map 中,以待后续操作使用。
如下图所示,至少两个客户端登录以后,才能正常视频通话。
watch -> 请求视频通话
点击 watch 按钮后,前端将发送一个事件到信令服务中,结构如下:
{
type: 'watch', //事件类型
from: 13789122381, // 我的账号,比如 13789122381
to: 1323493929 // 对方的账号,比如 1323493929
}
此时输入的对方账号对应 “to” 字段。
信令服务器收到 watch 事件后,从 map 中找出对应的在线客户端,将该事件转发至相应的客户端中。
offer -> 接通
对于接收者来说,点击“接通”按钮以后,webRTC 将开始建立通信隧道。
接通的 json 结构如下:
{
type: 'offer',
from: myId,
to: toId,
data: offer
}
整个拨打电话、接通的流程如下:
总结
在 html 中还需要配置 coturn TURN 服务 地址,我在 demo 中使用的地址是测试地址,所以请不要在生产中使用。
webRTC demo的更多相关文章
- WebRTC Demo - getUserMedia()
WebRTC介绍 WebRTC提供三类API: MediaStream,即getUserMedia RTCPeerConnection RTCDataChannel getUserMedia已经由Ch ...
- Qt WebRTC demo
This is a very simple demonstration of how to stream from a native application to the browser using ...
- WebRTC学习与DEMO资源一览
一. WebRTC学习 1.1 WebRTC现状 本人最早接触WebRTC是在2011年底,那时Google已经在Android源码中加入了webrtc源码,放在/external/webrtc/ ...
- Android IOS WebRTC 音视频开发总结(六二)-- 大数据解密国外实时通讯行业开发现状
本文主要介绍国外实时通讯行业现状,文章最早发表在我们的微信公众号上,详见这里,欢迎关注微信公众号blackerteam,更多详见www.blackerteam.com 上篇文章我们采用百度搜索指数来分 ...
- webrtc学习(一): webrtc开始
一. 编译webrtc 1. 预先准备 1) vpn. 用于同步代码. 这里给一个大概的估计吧. windows端包含vs2013 win8sdk wdk chromium源码等等, 总共需要至少8 ...
- WebRTC学习笔记_Demo收集
1. WebRTC学习 1.1 WebRTC现状 本人最早接触WebRTC是在2011年底,那时Google已经在Android源代码中增加了webrtc源代码,放在/external/w ...
- WebRTC–getUserMedia & Canvas
下面是一个使用getUserMedia接口和Canvas的drawImage方法实现的截图功能(截取视频中的一帧). 基本思路是这样子的: getUserMedia获取一个MediaStream, s ...
- iOS下WebRTC音视频通话(二)-局域网内音视频通话
这里是iOS 下WebRTC音视频通话开发的第二篇,在这一篇会利用一个局域网内音视频通话的例子介绍WebRTC中常用的API. 如果你下载并编译完成之后,会看到一个iOS 版的WebRTC Demo. ...
- WebRTC技术调研
相关网址: 协议:https://www.w3.org/TR/webrtc/ https://apprtc.webrtc.org/ https://apprtc.appspot.com/ https: ...
随机推荐
- Redis进阶篇:发布订阅模式原理与运用
"65 哥,如果你交了个漂亮小姐姐做女朋友,你会通过什么方式将这个消息广而告之给你的微信好友?" "那不得拍点女朋友的美照 + 亲密照弄一个九宫格图文消息在朋友圈发布大肆 ...
- mybatis-plus 生成全套crud
pom依赖: <!-- web依赖--> <dependency> <groupId>org.springframework.boot</groupId> ...
- Spring源码-入门
一.测试类 public class Main { public static void main(String[] args) { ApplicationContext applicationCon ...
- 【java】学习路径27-HashSet、TreeSet,HashMap
学习路径20-27的所有源代码都可以在此下载 https://www.aliyundrive.com/s/cg8jTRbg6vy HashSet.TreeSet中,Set表示集合,特性在于:无序的.不 ...
- 【PostgreSQL】PostgreSQL 15移除了Stats Collector
试用即将发行的PostgreSQL 15的人会发现少了一个后台进程: postgres 1710 1 0 04:03 ? 00:00:00 /usr/pgsql-15/bin/postmaster ...
- OpenJudge1.5.17 菲波那契数列
17:菲波那契数列 总时间限制: 1000ms 内存限制: 65536kB 描述 菲波那契数列是指这样的数列: 数列的第一个和第二个数都为1,接下来每个数都等于前面2个数之和. 给出一个正整数k,要求 ...
- 第九十二篇:Vue 自定义指令
好家伙, 1.什么是自定义指令? vue官方提供了v-text,v-for,v-model,v-if等常用的指令.除此之外vue还允许开发者自定义指令. 2.自定义指令的分类 vue中的自定义指令分为 ...
- KingbaseES V8R6 vacuum index_cleanup 选项
描述: 由于索引页的复用不像HEAP TABLE的PAGE复用机制那么简单只要有空闲空间就可以插入.索引页的空闲空间被复用,必须是PAGE的边界内的值才允许插入. 因此索引一旦膨胀,很难收缩,常用的方 ...
- HDFS 分布式环境搭建
HDFS 分布式环境搭建 作者:Grey 原文地址: 博客园:HDFS 分布式环境搭建 CSDN:HDFS 分布式环境搭建 准备四个Linux实例 每个实例对应的 host 和 ip 地址如下 nod ...
- 【读书笔记】C#高级编程 第十三章 异步编程
(一)异步编程的重要性 使用异步编程,方法调用是在后台运行(通常在线程或任务的帮助下),并不会阻塞调用线程.有3中不同的异步编程模式:异步模式.基于事件的异步模式和新增加的基于任务的异步模式(TAP, ...