参考书籍:HTML5 与 CSS3权威指南
实现让电话、电视及计算机都能够进行通信的公共平台,一个可以实现点对点视频聊天的Web应用程序,这就是WebRTC的目标。
WebRTC API是一个与getUserMedia方法紧密相关的API,它提供一种访问客户端本地的摄像头或麦克风设备的能力。
总体来说,WebRTC包含三个API:
WebRTC通信是指将实时取得的视频、音频等数据流(亦称字节流)在浏览器之间进行通信,即RTCPeerConnection。RTCPeerConnection具有两个特征:
为了在浏览器与浏览器之间进行P2P通信,必须首先知道对方的IP地址及动态分配的UDP端口号。因此在建立P2P通信之前,首先需要使用WebRTC交换一些信息。
(一)SDP:会话描述协议
SDP(Session Description Protocol)是一种会话描述协议,它以字符串的形式显示如下所示的一些浏览器信息:
(二)ICE:NAT穿越的协议(有点类似于内网穿透)
ICE(Interactive Connectivity Establishment)是一种在以UDP为基础的请求/回答模式的多媒体会话用于实现NAT穿越的协议。它以清单的形式描述P2P通信时可以使用的如下所示的一些通信途径:
ICE协议在网络上通过最短途径(网络负荷最小的途径)选择被发现的候选者,并按优先级依序列举这些候选者。
在P2P通信之前首先需要交换SDP信息与ICE信息,该过程称为“信令”。而进行信息交互的服务器成为信令服务器。
前端实现
<html>
<head>
<title>交换SDP信息与ICE信息title>
<meta name="viewport" content="width=device-width,
initial-scale=1,maximum-scale=1" />
head>
<body>
<button type="button" onclick="startVideo();">开始捕获视频信息button>
<button type="button" onclick="stopVideo();">停止捕获视频信息button>
<button type="button" onclick="connect();">建立连接button>
<button type="button" onclick="hangUp();">挂断button>
<br />
<div>
<video id="local-video" autoplay style="width: 240px; height: 180px;
border: 1px solid black;">video>
<video id="remote-video" autoplay style="width: 240px; height: 180px;
border: 1px solid black;">video>
div>
<p>
<script>
// ===================以下是socket=======================
var user = Math.round(Math.random()*1000) + ""
var socketUrl = "ws://127.0.0.1:8080/msgServer/"+user;
var socket = null
var socketRead = false
window.onload = function() {
socket = new WebSocket(socketUrl)
socket.onopen = function() {
console.log("成功连接到服务器...")
socketRead = true
}
socket.onclose = function(e) {
console.log('与服务器连接关闭: ' + e.code)
socketRead = false
}
socket.onmessage = function(res) {
var evt = JSON.parse(res.data)
console.log(evt)
if (evt.type === 'offer') {
console.log("接收到offer,设置offer,发送answer....")
onOffer(evt);
} else if (evt.type === 'answer' && peerStarted) {
console.log('接收到answer,设置answer SDP');
onAnswer(evt);
} else if (evt.type === 'candidate' && peerStarted) {
console.log('接收到ICE候选者..');
onCandidate(evt);
} else if (evt.type === 'bye' && peerStarted) {
console.log("WebRTC通信断开");
stop();
}
}
}
// ===================以上是socket=======================
var localVideo = document.getElementById('local-video');
var remoteVideo = document.getElementById('remote-video');
var localStream = null;
var peerConnection = null;
var peerStarted = false;
var mediaConstraints = {
'mandatory': {
'OfferToReceiveAudio': false,
'OfferToReceiveVideo': true
}
};
//----------------------交换信息 -----------------------
function onOffer(evt) {
console.log("接收到offer...")
console.log(evt);
setOffer(evt);
sendAnswer(evt);
peerStarted = true
}
function onAnswer(evt) {
console.log("接收到Answer...")
console.log(evt);
setAnswer(evt);
}
function onCandidate(evt) {
var candidate = new RTCIceCandidate({
sdpMLineIndex: evt.sdpMLineIndex,
sdpMid: evt.sdpMid, candidate: evt.candidate
});
console.log("接收到Candidate...")
console.log(candidate);
peerConnection.addIceCandidate(candidate);
}
function sendSDP(sdp) {
var text = JSON.stringify(sdp);
console.log('发送sdp.....')
console.log(text); // "type":"offer"....
// textForSendSDP.value = text;
// 通过socket发送sdp
socket.send(text)
}
function sendCandidate(candidate) {
var text = JSON.stringify(candidate);
console.log(text);// "type":"candidate","sdpMLineIndex":0,"sdpMid":"0","candidate":"....
socket.send(text)// socket发送
}
//---------------------- 视频处理 -----------------------
function startVideo() {
navigator.webkitGetUserMedia({ video: true, audio: false },
function (stream) { //success
localStream = stream;
localVideo.srcObject = stream;
//localVideo.src = window.URL.createObjectURL(stream);
localVideo.play();
localVideo.volume = 0;
},
function (error) { //error
console.error('发生了一个错误: [错误代码:' + error.code + ']');
return;
});
}
function stopVideo() {
localVideo.src = "";
localStream.stop();
}
//---------------------- 处理连接 -----------------------
function prepareNewConnection() {
var pc_config = { "iceServers": [] };
var peer = null;
try {
peer = new webkitRTCPeerConnection(pc_config);
}
catch (e) {
console.log("建立连接失败,错误:" + e.message);
}
// 发送所有ICE候选者给对方
peer.onicecandidate = function (evt) {
if (evt.candidate) {
console.log(evt.candidate);
sendCandidate({
type: "candidate",
sdpMLineIndex: evt.candidate.sdpMLineIndex,
sdpMid: evt.candidate.sdpMid,
candidate: evt.candidate.candidate
});
}
};
console.log('添加本地视频流...');
peer.addStream(localStream);
peer.addEventListener("addstream", onRemoteStreamAdded, false);
peer.addEventListener("removestream", onRemoteStreamRemoved, false);
// 当接收到远程视频流时,使用本地video元素进行显示
function onRemoteStreamAdded(event) {
console.log("添加远程视频流");
// remoteVideo.src = window.URL.createObjectURL(event.stream);
remoteVideo.srcObject = event.stream;
}
// 当远程结束通信时,取消本地video元素中的显示
function onRemoteStreamRemoved(event) {
console.log("移除远程视频流");
remoteVideo.src = "";
}
return peer;
}
function sendOffer() {
peerConnection = prepareNewConnection();
peerConnection.createOffer(function (sessionDescription) { //成功时调用
peerConnection.setLocalDescription(sessionDescription);
console.log("发送: SDP");
console.log(sessionDescription);
sendSDP(sessionDescription);
}, function (err) { //失败时调用
console.log("创建Offer失败");
}, mediaConstraints);
}
function setOffer(evt) {
if (peerConnection) {
console.error('peerConnection已存在!');
return;
}
peerConnection = prepareNewConnection();
peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
}
function sendAnswer(evt) {
console.log('发送Answer,创建远程会话描述...');
if (!peerConnection) {
console.error('peerConnection不存在!');
return;
}
peerConnection.createAnswer(function (sessionDescription) {//成功时
peerConnection.setLocalDescription(sessionDescription);
console.log("发送: SDP");
console.log(sessionDescription);
sendSDP(sessionDescription);
}, function () { //失败时
console.log("创建Answer失败");
}, mediaConstraints);
}
function setAnswer(evt) {
if (!peerConnection) {
console.error('peerConnection不存在!');
return;
}
peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
}
//-------- 处理用户UI事件 -----
// 开始建立连接
function connect() {
if (!peerStarted && localStream && socketRead) {
sendOffer();
peerStarted = true;
} else {
alert("请首先捕获本地视频数据.");
}
}
// 停止连接
function hangUp() {
console.log("挂断.");
stop();
}
function stop() {
peerConnection.close();
peerConnection = null;
peerStarted = false;
}
script>
body>
html>
后端实现
package com.one.socket.socketserver;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Enumeration;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/msgServer/{userId}")
@Component
@Scope("prototype")
public class WebSocketServer {
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static ConcurrentHashMap<String, Session> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String userId = "";
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
/**
* 连接被打开:向socket-map中添加session
*/
webSocketMap.put(userId, session);
System.out.println(userId + " - 连接建立成功...");
}
@OnMessage
public void onMessage(String message, Session session) {
try {
this.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
@OnError
public void onError(Session session, Throwable error) {
System.out.println("连接异常...");
error.printStackTrace();
}
@OnClose
public void onClose() {
System.out.println("连接关闭");
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
if (message.equals("心跳")){
this.session.getBasicRemote().sendText(message);
}
Enumeration<String> keys = webSocketMap.keys();
while (keys.hasMoreElements()){
String key = keys.nextElement();
if (key.equals(this.userId)){
System.err.println("my id " + key);
continue;
}
if (webSocketMap.get(key) == null){
webSocketMap.remove(key);
System.err.println(key + " : null");
continue;
}
Session sessionValue = webSocketMap.get(key);
if (sessionValue.isOpen()){
System.out.println("发消息给: " + key + " ,message: " + message);
sessionValue.getBasicRemote().sendText(message);
}else {
System.err.println(key + ": not open");
sessionValue.close();
webSocketMap.remove(key);
}
}
}
/**
* 发送自定义消息
*/
public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException {
System.out.println("发送消息到:" + userId + ",内容:" + message);
if (!StringUtils.isEmpty(userId) && webSocketMap.containsKey(userId)) {
webSocketMap.get(userId).getBasicRemote().sendText(message);
//webSocketServer.sendMessage(message);
} else {
System.out.println("用户" + userId + ",不在线!");
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
说明:视频通话需要开启两个窗口,首先在A、B窗口分别开启视频捕获,然后将A窗口作为通话发起方,点击建立连接,便实现了自己与自己进行视频通话,所以两个窗口中的内容是相同的。