一、环境配置
1. 安装了Kurento Media Server(KMS)的 Ubuntu16.04(Xenial)。
首先解决服务系统,若有腾讯云或阿里云的Ubuntu16服务器则直接安装KMS服务;没有的可通过VMware虚拟机在本地机器上安装Ubuntu16.04(Xenial)之后再安装KMS服务;
a. 先说没有云服务器的情况,没有虚拟机的可通过本地址下载https://www.vmware.com/products/workstation-pro/workstation-pro-evaluation.html?productId=686&rPId=25455;
没有Ubuntu的可通过http://releases.ubuntu.com/16.04下载注意选择无窗口的版本ubuntu-16.04.5-server-amd64.iso;
遇到的情况:在虚拟机上安装系统时可能会遇到“Intel VT-x 处于禁用状态”,百度经验https://jingyan.baidu.com/article/fc07f98976710e12ffe519de.html;装好系统后先切换到管理员权限下,先设置root密码命令:
sudo passwd root
输入root的密码和确认密码,之后在执行命令:
su root
输入刚才设置新密码;新装的Ubuntu是没有vim编辑器的执行命令自动安装:
apt-get install vim
b.服务器有了之后便可安装KMS服务了依次执行以下命令便可
REPO="xenial"
echo "deb http://ubuntu.kurento.org $REPO kms6" | sudo tee /etc/apt/sources.list.d/kurento.list
wget http://ubuntu.kurento.org/kurento.gpg.key -O - | sudo apt-key add -
apt update
useradd -U -m kurento
apt install -y kurento-media-server-6.0
systemctl start kurento-media-server-6.0
安装成功后KMS默认运行在8888端口通过命令: netstat -tunlp|grep 8888 查看;
配置STUN和TURN服务器,执行命令:
vim /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini
编辑
stunServerAddress = <serverIp>
stunServerPort = <serverPort>
为:(注意每行前面没有分号)
stunServerAddress = 77.72.174.163
stunServerPort = 3478
添加 H.264 支持 执行命令:
apt install -y openh264-gst-plugins-bad-1.5
重启 kurento server
systemctl restart kurento-media-server-6.0
到 此KMS服务安装完毕。
2. 基于spring boot 的KurentoClient。
a.先来一个spring boot项目,打开链接 https://start.spring.io/ ,设置选项如图,点击绿色按钮下载一个空的spring boot项目然后导入编辑器,
配置application.properties
# LOGGING
logging.level.root=INFO
logging.level.org.apache=WARN
logging.level.org.springframework=WARN
logging.level.org.kurento=INFO
logging.level.org.kurento.tutorial=INFO
# OUTPUT
# Terminal color output; one of [ALWAYS, DETECT, NEVER]
spring.output.ansi.enabled=DETECT
# ----------------------------------------
# WEB PROPERTIES
# ----------------------------------------
# EMBEDDED SERVER CONFIGURATION
server.port=8443
配置pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.kurento</groupId>
<artifactId>kurento-client</artifactId>
<version>6.7.1</version>
</dependency>
<dependency>
<groupId>org.kurento</groupId>
<artifactId>kurento-utils-js</artifactId>
<version>6.7.0</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
<version>0.34</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId >
<artifactId>bootstrap</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>adapter.js</artifactId >
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>ekko-lightbox</artifactId>
<version>5.2.0</version>
</dependency>
</dependencies>
CallMediaPipeline.java
import java.text.SimpleDateFormat;
import java.util.Date;
import org.kurento.client.KurentoClient;
import org.kurento.client.MediaPipeline;
import org.kurento.client.RecorderEndpoint;
import org.kurento.client.WebRtcEndpoint;
public class CallMediaPipeline {
private static final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss-S");
public static final String RECORDING_PATH = "file:///tmp/" + df.format(new Date()) + "-";
public static final String RECORDING_EXT = ".webm";
private final MediaPipeline pipeline;
private final WebRtcEndpoint webRtcCaller;
private final WebRtcEndpoint webRtcCallee;
private final RecorderEndpoint recorderCaller;
private final RecorderEndpoint recorderCallee;
public CallMediaPipeline(KurentoClient kurento, String from, String to) {
// Media pipeline
pipeline = kurento.createMediaPipeline();
// Media Elements (WebRtcEndpoint, RecorderEndpoint)
webRtcCaller = new WebRtcEndpoint.Builder(pipeline).build();
webRtcCallee = new WebRtcEndpoint.Builder(pipeline).build();
recorderCaller = new RecorderEndpoint.Builder(pipeline, RECORDING_PATH + from + RECORDING_EXT)
.build();
recorderCallee = new RecorderEndpoint.Builder(pipeline, RECORDING_PATH + to + RECORDING_EXT)
.build();
// Connections
webRtcCaller.connect(webRtcCallee);
webRtcCaller.connect(recorderCaller);
webRtcCallee.connect(webRtcCaller);
webRtcCallee.connect(recorderCallee);
}
public void record() {
recorderCaller.record();
recorderCallee.record();
}
public String generateSdpAnswerForCaller(String sdpOffer) {
return webRtcCaller.processOffer(sdpOffer);
}
public String generateSdpAnswerForCallee(String sdpOffer) {
return webRtcCallee.processOffer(sdpOffer);
}
public MediaPipeline getPipeline() {
return pipeline;
}
public WebRtcEndpoint getCallerWebRtcEp() {
return webRtcCaller;
}
public WebRtcEndpoint getCalleeWebRtcEp() {
return webRtcCallee;
}
}
One2OneCallRecApp.java
package org.kurento.tutorial.one2onecallrec;
import org.kurento.client.KurentoClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@SpringBootApplication
@EnableWebSocket
public class One2OneCallRecApp implements WebSocketConfigurer {
@Bean
public CallHandler callHandler() {
return new CallHandler();
}
@Bean
public UserRegistry registry() {
return new UserRegistry();
}
@Bean
public KurentoClient kurentoClient() {
//此处IP地址为安装了KMS服务的Ubuntu 16 IP地址,若服务器为https的则'ws'改为‘wss’
//若KMS安装在VMware上则为该虚拟机在局域网的IP
return KurentoClient.create("ws://192.168.0.128:8888/kurento");
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(callHandler(), "/call").setAllowedOrigins("*");
}
public static void main(String[] args) throws Exception {
SpringApplication.run(One2OneCallRecApp.class, args);
}
}
OneToOneCallHandler.java
package org.kurento.tutorial.one2onecallrec;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import org.kurento.client.EndOfStreamEvent;
import org.kurento.client.EventListener;
import org.kurento.client.IceCandidate;
import org.kurento.client.IceCandidateFoundEvent;
import org.kurento.client.KurentoClient;
import org.kurento.client.MediaPipeline;
import org.kurento.jsonrpc.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
public class CallHandler extends TextWebSocketHandler {
private static final Logger log = LoggerFactory.getLogger(CallHandler.class);
private static final Gson gson = new GsonBuilder().create();
private final ConcurrentHashMap<String, MediaPipeline> pipelines = new ConcurrentHashMap<>();
@Autowired
private KurentoClient kurento;
@Autowired
private UserRegistry registry;
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
JsonObject jsonMessage = gson.fromJson(message.getPayload(), JsonObject.class);
UserSession user = registry.getBySession(session);
if (user != null) {
log.debug("Incoming message from user '{}': {}", user.getName(), jsonMessage);
} else {
log.debug("Incoming message from new user: {}", jsonMessage);
}
switch (jsonMessage.get("id").getAsString()) {
case "register":
register(session, jsonMessage);
break;
case "call":
call(user, jsonMessage);
break;
case "incomingCallResponse":
incomingCallResponse(user, jsonMessage);
break;
case "play":
play(user, jsonMessage);
break;
case "onIceCandidate": {
JsonObject candidate = jsonMessage.get("candidate").getAsJsonObject();
if (user != null) {
IceCandidate cand =
new IceCandidate(candidate.get("candidate").getAsString(), candidate.get("sdpMid")
.getAsString(), candidate.get("sdpMLineIndex").getAsInt());
user.addCandidate(cand);
}
break;
}
case "stop":
stop(session);
releasePipeline(user);
break;
case "stopPlay":
releasePipeline(user);
break;
default:
break;
}
}
private void register(WebSocketSession session, JsonObject jsonMessage) throws IOException {
String name = jsonMessage.getAsJsonPrimitive("name").getAsString();
UserSession caller = new UserSession(session, name);
String responseMsg = "accepted";
if (name.isEmpty()) {
responseMsg = "rejected: empty user name";
} else if (registry.exists(name)) {
responseMsg = "rejected: user '" + name + "' already registered";
} else {
registry.register(caller);
}
JsonObject response = new JsonObject();
response.addProperty("id", "registerResponse");
response.addProperty("response", responseMsg);
caller.sendMessage(response);
}
private void call(UserSession caller, JsonObject jsonMessage) throws IOException {
String to = jsonMessage.get("to").getAsString();
String from = jsonMessage.get("from").getAsString();
JsonObject response = new JsonObject();
if (registry.exists(to)) {
caller.setSdpOffer(jsonMessage.getAsJsonPrimitive("sdpOffer").getAsString());
caller.setCallingTo(to);
response.addProperty("id", "incomingCall");
response.addProperty("from", from);
UserSession callee = registry.getByName(to);
callee.sendMessage(response);
callee.setCallingFrom(from);
} else {
response.addProperty("id", "callResponse");
response.addProperty("response", "rejected");
response.addProperty("message", "user '" + to + "' is not registered");
caller.sendMessage(response);
}
}
private void incomingCallResponse(final UserSession callee, JsonObject jsonMessage)
throws IOException {
String callResponse = jsonMessage.get("callResponse").getAsString();
String from = jsonMessage.get("from").getAsString();
final UserSession calleer = registry.getByName(from);
String to = calleer.getCallingTo();
if ("accept".equals(callResponse)) {
log.debug("Accepted call from '{}' to '{}'", from, to);
CallMediaPipeline callMediaPipeline = new CallMediaPipeline(kurento, from, to);
pipelines.put(calleer.getSessionId(), callMediaPipeline.getPipeline());
pipelines.put(callee.getSessionId(), callMediaPipeline.getPipeline());
callee.setWebRtcEndpoint(callMediaPipeline.getCalleeWebRtcEp());
callMediaPipeline.getCalleeWebRtcEp().addIceCandidateFoundListener(
new EventListener<IceCandidateFoundEvent>() {
@Override
public void onEvent(IceCandidateFoundEvent event) {
JsonObject response = new JsonObject();
response.addProperty("id", "iceCandidate");
response.add("candidate", JsonUtils.toJsonObject(event.getCandidate()));
try {
synchronized (callee.getSession()) {
callee.getSession().sendMessage(new TextMessage(response.toString()));
}
} catch (IOException e) {
log.debug(e.getMessage());
}
}
});
String calleeSdpOffer = jsonMessage.get("sdpOffer").getAsString();
String calleeSdpAnswer = callMediaPipeline.generateSdpAnswerForCallee(calleeSdpOffer);
JsonObject startCommunication = new JsonObject();
startCommunication.addProperty("id", "startCommunication");
startCommunication.addProperty("sdpAnswer", calleeSdpAnswer);
synchronized (callee) {
callee.sendMessage(startCommunication);
}
callMediaPipeline.getCalleeWebRtcEp().gatherCandidates();
String callerSdpOffer = registry.getByName(from).getSdpOffer();
calleer.setWebRtcEndpoint(callMediaPipeline.getCallerWebRtcEp());
callMediaPipeline.getCallerWebRtcEp().addIceCandidateFoundListener(
new EventListener<IceCandidateFoundEvent>() {
@Override
public void onEvent(IceCandidateFoundEvent event) {
JsonObject response = new JsonObject();
response.addProperty("id", "iceCandidate");
response.add("candidate", JsonUtils.toJsonObject(event.getCandidate()));
try {
synchronized (calleer.getSession()) {
calleer.getSession().sendMessage(new TextMessage(response.toString()));
}
} catch (IOException e) {
log.debug(e.getMessage());
}
}
});
String callerSdpAnswer = callMediaPipeline.generateSdpAnswerForCaller(callerSdpOffer);
JsonObject response = new JsonObject();
response.addProperty("id", "callResponse");
response.addProperty("response", "accepted");
response.addProperty("sdpAnswer", callerSdpAnswer);
synchronized (calleer) {
calleer.sendMessage(response);
}
callMediaPipeline.getCallerWebRtcEp().gatherCandidates();
callMediaPipeline.record();
} else {
JsonObject response = new JsonObject();
response.addProperty("id", "callResponse");
response.addProperty("response", "rejected");
calleer.sendMessage(response);
}
}
public void stop(WebSocketSession session) throws IOException {
// Both users can stop the communication. A 'stopCommunication'
// message will be sent to the other peer.
UserSession stopperUser = registry.getBySession(session);
if (stopperUser != null) {
UserSession stoppedUser =
(stopperUser.getCallingFrom() != null) ? registry.getByName(stopperUser.getCallingFrom())
: stopperUser.getCallingTo() != null ? registry.getByName(stopperUser.getCallingTo())
: null;
if (stoppedUser != null) {
JsonObject message = new JsonObject();
message.addProperty("id", "stopCommunication");
stoppedUser.sendMessage(message);
stoppedUser.clear();
}
stopperUser.clear();
}
}
public void releasePipeline(UserSession session) {
String sessionId = session.getSessionId();
if (pipelines.containsKey(sessionId)) {
pipelines.get(sessionId).release();
pipelines.remove(sessionId);
}
session.setWebRtcEndpoint(null);
session.setPlayingWebRtcEndpoint(null);
// set to null the endpoint of the other user
UserSession stoppedUser =
(session.getCallingFrom() != null) ? registry.getByName(session.getCallingFrom())
: registry.getByName(session.getCallingTo());
stoppedUser.setWebRtcEndpoint(null);
stoppedUser.setPlayingWebRtcEndpoint(null);
}
private void play(final UserSession session, JsonObject jsonMessage) throws IOException {
String user = jsonMessage.get("user").getAsString();
log.debug("Playing recorded call of user '{}'", user);
JsonObject response = new JsonObject();
response.addProperty("id", "playResponse");
if (registry.getByName(user) != null && registry.getBySession(session.getSession()) != null) {
final PlayMediaPipeline playMediaPipeline =
new PlayMediaPipeline(kurento, user, session.getSession());
session.setPlayingWebRtcEndpoint(playMediaPipeline.getWebRtc());
playMediaPipeline.getPlayer().addEndOfStreamListener(new EventListener<EndOfStreamEvent>() {
@Override
public void onEvent(EndOfStreamEvent event) {
UserSession user = registry.getBySession(session.getSession());
releasePipeline(user);
playMediaPipeline.sendPlayEnd(session.getSession());
}
});
playMediaPipeline.getWebRtc().addIceCandidateFoundListener(
new EventListener<IceCandidateFoundEvent>() {
@Override
public void onEvent(IceCandidateFoundEvent event) {
JsonObject response = new JsonObject();
response.addProperty("id", "iceCandidate");
response.add("candidate", JsonUtils.toJsonObject(event.getCandidate()));
try {
synchronized (session) {
session.getSession().sendMessage(new TextMessage(response.toString()));
}
} catch (IOException e) {
log.debug(e.getMessage());
}
}
});
String sdpOffer = jsonMessage.get("sdpOffer").getAsString();
String sdpAnswer = playMediaPipeline.generateSdpAnswer(sdpOffer);
response.addProperty("response", "accepted");
response.addProperty("sdpAnswer", sdpAnswer);
playMediaPipeline.play();
pipelines.put(session.getSessionId(), playMediaPipeline.getPipeline());
synchronized (session.getSession()) {
session.sendMessage(response);
}
playMediaPipeline.getWebRtc().gatherCandidates();
} else {
response.addProperty("response", "rejected");
response.addProperty("error", "No recording for user '" + user
+ "'. Please type a correct user in the 'Peer' field.");
session.getSession().sendMessage(new TextMessage(response.toString()));
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
stop(session);
registry.removeBySession(session);
}
}
OneToOneUserRegistry.java
package org.kurento.tutorial.one2onecallrec;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.web.socket.WebSocketSession;
public class UserRegistry {
private ConcurrentHashMap<String, UserSession> usersByName = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, UserSession> usersBySessionId = new ConcurrentHashMap<>();
public void register(UserSession user) {
usersByName.put(user.getName(), user);
usersBySessionId.put(user.getSession().getId(), user);
}
public UserSession getByName(String name) {
return usersByName.get(name);
}
public UserSession getBySession(WebSocketSession session) {
return usersBySessionId.get(session.getId());
}
public boolean exists(String name) {
return usersByName.keySet().contains(name);
}
public UserSession removeBySession(WebSocketSession session) {
final UserSession user = getBySession(session);
if (user != null) {
usersByName.remove(user.getName());
usersBySessionId.remove(session.getId());
}
return user;
}
}
OneToOneUserSession.java
package org.kurento.tutorial.one2onecallrec;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.kurento.client.IceCandidate;
import org.kurento.client.WebRtcEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import com.google.gson.JsonObject;
public class UserSession {
private static final Logger log = LoggerFactory.getLogger(UserSession.class);
private final String name;
private final WebSocketSession session;
private String sdpOffer;
private String callingTo;
private String callingFrom;
private WebRtcEndpoint webRtcEndpoint;
private WebRtcEndpoint playingWebRtcEndpoint;
private final List<IceCandidate> candidateList = new ArrayList<>();
public UserSession(WebSocketSession session, String name) {
this.session = session;
this.name = name;
}
public WebSocketSession getSession() {
return session;
}
public String getName() {
return name;
}
public String getSdpOffer() {
return sdpOffer;
}
public void setSdpOffer(String sdpOffer) {
this.sdpOffer = sdpOffer;
}
public String getCallingTo() {
return callingTo;
}
public void setCallingTo(String callingTo) {
this.callingTo = callingTo;
}
public String getCallingFrom() {
return callingFrom;
}
public void setCallingFrom(String callingFrom) {
this.callingFrom = callingFrom;
}
public void sendMessage(JsonObject message) throws IOException {
log.debug("Sending message from user '{}': {}", name, message);
session.sendMessage(new TextMessage(message.toString()));
}
public String getSessionId() {
return session.getId();
}
public void setWebRtcEndpoint(WebRtcEndpoint webRtcEndpoint) {
this.webRtcEndpoint = webRtcEndpoint;
if (this.webRtcEndpoint != null) {
for (IceCandidate e : candidateList) {
this.webRtcEndpoint.addIceCandidate(e);
}
this.candidateList.clear();
}
}
public void addCandidate(IceCandidate candidate) {
if (this.webRtcEndpoint != null) {
this.webRtcEndpoint.addIceCandidate(candidate);
} else {
candidateList.add(candidate);
}
if (this.playingWebRtcEndpoint != null) {
this.playingWebRtcEndpoint.addIceCandidate(candidate);
}
}
public WebRtcEndpoint getPlayingWebRtcEndpoint() {
return playingWebRtcEndpoint;
}
public void setPlayingWebRtcEndpoint(WebRtcEndpoint playingWebRtcEndpoint) {
this.playingWebRtcEndpoint = playingWebRtcEndpoint;
}
public void clear() {
this.webRtcEndpoint = null;
this.candidateList.clear();
}
}
PlayMediaPipeline.java
package org.kurento.tutorial.one2onecallrec;
import static org.kurento.tutorial.one2onecallrec.CallMediaPipeline.RECORDING_EXT;
import static org.kurento.tutorial.one2onecallrec.CallMediaPipeline.RECORDING_PATH;
import java.io.IOException;
import org.kurento.client.ErrorEvent;
import org.kurento.client.EventListener;
import org.kurento.client.KurentoClient;
import org.kurento.client.MediaPipeline;
import org.kurento.client.PlayerEndpoint;
import org.kurento.client.WebRtcEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import com.google.gson.JsonObject;
public class PlayMediaPipeline {
private static final Logger log = LoggerFactory.getLogger(PlayMediaPipeline.class);
private final MediaPipeline pipeline;
private WebRtcEndpoint webRtc;
private final PlayerEndpoint player;
public PlayMediaPipeline(KurentoClient kurento, String user, final WebSocketSession session) {
// Media pipeline
pipeline = kurento.createMediaPipeline();
// Media Elements (WebRtcEndpoint, PlayerEndpoint)
webRtc = new WebRtcEndpoint.Builder(pipeline).build();
player = new PlayerEndpoint.Builder(pipeline, RECORDING_PATH + user + RECORDING_EXT).build();
// Connection
player.connect(webRtc);
// Player listeners
player.addErrorListener(new EventListener<ErrorEvent>() {
@Override
public void onEvent(ErrorEvent event) {
log.info("ErrorEvent: {}", event.getDescription());
sendPlayEnd(session);
}
});
}
public void sendPlayEnd(WebSocketSession session) {
try {
JsonObject response = new JsonObject();
response.addProperty("id", "playEnd");
session.sendMessage(new TextMessage(response.toString()));
} catch (IOException e) {
log.error("Error sending playEndOfStream message", e);
}
// Release pipeline
pipeline.release();
this.webRtc = null;
}
public void play() {
player.play();
}
public String generateSdpAnswer(String sdpOffer) {
return webRtc.processOffer(sdpOffer);
}
public MediaPipeline getPipeline() {
return pipeline;
}
public WebRtcEndpoint getWebRtc() {
return webRtc;
}
public PlayerEndpoint getPlayer() {
return player;
}
}
添加完六个Java文档后若启动不报错则说名成功连接上了KMS服务 。
b.后台的工作做完了,来前端的
编辑onetoone.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="expires" content="0">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="./img/kurento.png" type="image/png" />
<link rel="stylesheet"
href="webjars/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet"
href="webjars/ekko-lightbox/dist/ekko-lightbox.min.css">
<link rel="stylesheet" href="webjars/demo-console/index.css">
<link rel="stylesheet" href="css/kurento.css">
<script src="webjars/jquery/dist/jquery.min.js"></script>
<script src="webjars/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="webjars/ekko-lightbox/dist/ekko-lightbox.min.js"></script>
<script src="https://draggabilly.desandro.com/draggabilly.pkgd.min.js"></script>
<script src="./js/adapter.js"></script>
<script src="./js/console.js"></script>
<script src="./js/kurento-utils.js"></script>
<script src="./js/one2one.js"></script>
<title>Kurento Tutorial: Video Call 1 to 1 with WebRTC with
recording</title>
</head>
<body>
<header>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse"
data-target=".navbar-collapse"></button>
<a class="navbar-brand" href="./">Kurento Tutorial</a>
</div>
<div class="collapse navbar-collapse"
id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li><a
href="https://github.com/Kurento/kurento-tutorial-java/tree/master/kurento-one2one-call-recording"><span
class="glyphicon glyphicon-file"></span> Source Code</a></li>
</ul>
</div>
</div>
</div>
</header>
<div class="container">
<div class="page-header">
<h1>Tutorial: Video Call 1 to 1 with recording</h1>
<p>
This web application consists on an one to one video call using <a
href="http://www.webrtc.org/">WebRTC</a>. It uses the Kurento
capabilities or recording of the video communication. This
application implements two different Media Pipelines. The <a
href="./img/pipeline1.png" data-toggle="lightbox"
data-title="Video Call 1 to 1 with recording, First Media Pipeline"
data-footer="Two interconnected WebRtcEnpoints Media Elements with recording (RecorderEndpoint)">first
Media Pipeline</a> is used to communicate two peers and it is composed
by two interconnected <i>WebRtcEndpoints</i> with two <i>RecorderEndpoints</i>
to carry out the recording. The recorded stream will be stored in
the file system of the Kurento Media Server. Then, a <a
href="./img/pipeline2.png" data-toggle="lightbox"
data-title=" Video Call 1 to 1 with recordging, Second Media Pipeline"
data-footer="A PlayerEndpoint (reading the recorded file in the Kurento Media Server) connected to a WebRtcEnpoint in receive-only mode">second
Media Pipeline</a> is used to play the recorded media. To run this demo
follow these steps:
</p>
<ol>
<li>Open this page with a browser compliant with WebRTC
(Chrome, Firefox).</li>
<li>Type a nick in the field <i>Name</i> and click on <i>Register</i>.
</li>
<li>In a different machine (or a different tab in the same
browser) follow the same procedure to register another user.</li>
<li>Type the name of the user to be called in the field <i>Peer</i>
and click on <i>Call</i>.
</li>
<li>Grant the access to the camera and microphone for both
users. After the SDP negotiation the communication should start.</li>
<li>The called user should accept the incoming call (by a
confirmation dialog).</li>
<li>Click on <i>Stop</i> to finish the communication.
</li>
<li>Type the name of the user to play its recording in the
field <i>Peer</i> and click on <i>Play Rec</i>
</li>
</ol>
</div>
<div class="row">
<div class="col-md-5">
<label class="control-label" for="name">Name</label>
<div class="row">
<div class="col-md-5">
<input id="name" name="name" class="form-control" type="text"
onkeydown="if (event.keyCode == 13) register();" />
</div>
<div class="col-md-7 text-right">
<a id="register" href="#" class="btn btn-primary"><span
class="glyphicon glyphicon-plus"></span> Register</a>
</div>
</div>
<br /> <br /> <label class="control-label" for="peer">Peer</label>
<div class="row">
<div class="col-md-5">
<input id="peer" name="peer" class="form-control" type="text"
onkeydown="if (event.keyCode == 13) call();">
</div>
<div class="col-md-7 text-right">
<a id="call" href="#" class="btn btn-success"><span
class="glyphicon glyphicon-play"></span> Call</a> <a id="terminate"
href="#" class="btn btn-danger"><span
class="glyphicon glyphicon-stop"></span> Stop</a> <a id="play"
href="#" class="btn btn-warning"><span
class="glyphicon glyphicon-play-circle"></span>Play Rec</a>
</div>
</div>
<br /> <label class="control-label" for="console">Console</label><br>
<br>
<div id="console" class="democonsole">
<ul></ul>
</div>
</div>
<div class="col-md-7">
<div id="videoBig">
<video id="videoOutput" autoplay width="640px" height="480px"
poster="./img/webrtc.png"></video>
</div>
<div id="videoSmall">
<video id="videoInput" autoplay width="240px" height="180px"
poster="./img/webrtc.png"></video>
</div>
</div>
</div>
</div>
<footer>
<div class="foot-fixed-bottom">
<div class="container text-center">
<hr />
<div class="row">© 2014-2015 Kurento</div>
<div class="row">
<div class="col-md-4">
<a href="http://www.urjc.es"><img src="./img/urjc.gif"
alt="Universidad Rey Juan Carlos" height="50px" /></a>
</div>
<div class="col-md-4">
<a href="http://www.kurento.org"><img src="./img/kurento.png"
alt="Kurento" height="50px" /></a>
</div>
<div class="col-md-4">
<a href="http://www.naevatec.com"><img
src="./img/naevatec.png" alt="Naevatec" height="50px" /></a>
</div>
</div>
</div>
</div>
</footer>
</body>
</html>
one2one.js
var ws = new WebSocket('ws://' + location.host + '/call');
var videoInput;
var videoOutput;
var webRtcPeer;
var from;
var registerName = null;
var registerState = null;
const NOT_REGISTERED = 0;
const REGISTERING = 1;
const REGISTERED = 2;
function setRegisterState(nextState) {
switch (nextState) {
case NOT_REGISTERED:
enableButton('#register', 'register()');
setCallState(DISABLED);
break;
case REGISTERING:
disableButton('#register');
break;
case REGISTERED:
disableButton('#register');
setCallState(NO_CALL);
break;
default:
return;
}
registerState = nextState;
}
var callState = null;
const NO_CALL = 0;
const IN_CALL = 1;
const POST_CALL = 2;
const DISABLED = 3;
const IN_PLAY = 4;
function setCallState(nextState) {
switch (nextState) {
case NO_CALL:
enableButton('#call', 'call()');
disableButton('#terminate');
disableButton('#play');
break;
case DISABLED:
disableButton('#call');
disableButton('#terminate');
disableButton('#play');
break;
case POST_CALL:
enableButton('#call', 'call()');
disableButton('#terminate');
enableButton('#play', 'play()');
break;
case IN_CALL:
case IN_PLAY:
disableButton('#call');
enableButton('#terminate', 'stop()');
disableButton('#play');
break;
default:
return;
}
callState = nextState;
}
function disableButton(id) {
$(id).attr('disabled', true);
$(id).removeAttr('onclick');
}
function enableButton(id, functionName) {
$(id).attr('disabled', false);
$(id).attr('onclick', functionName);
}
window.onload = function() {
console = new Console();
setRegisterState(NOT_REGISTERED);
var drag = new Draggabilly(document.getElementById('videoSmall'));
videoInput = document.getElementById('videoInput');
videoOutput = document.getElementById('videoOutput');
document.getElementById('name').focus();
}
window.onbeforeunload = function() {
ws.close();
}
ws.onmessage = function(message) {
var parsedMessage = JSON.parse(message.data);
console.info('Received message: ' + message.data);
switch (parsedMessage.id) {
case 'registerResponse':
registerResponse(parsedMessage);
break;
case 'callResponse':
callResponse(parsedMessage);
break;
case 'incomingCall':
incomingCall(parsedMessage);
break;
case 'startCommunication':
startCommunication(parsedMessage);
break;
case 'stopCommunication':
console.info('Communication ended by remote peer');
stop(true);
break;
case 'playResponse':
playResponse(parsedMessage);
break;
case 'playEnd':
playEnd();
break;
case 'iceCandidate':
webRtcPeer.addIceCandidate(parsedMessage.candidate, function(error) {
if (error)
return console.error('Error adding candidate: ' + error);
});
break;
default:
console.error('Unrecognized message', parsedMessage);
}
}
function registerResponse(message) {
if (message.response == 'accepted') {
setRegisterState(REGISTERED);
document.getElementById('peer').focus();
} else {
setRegisterState(NOT_REGISTERED);
var errorMessage = message.response ? message.response
: 'Unknown reason for register rejection.';
console.log(errorMessage);
document.getElementById('name').focus();
alert('Error registering user. See console for further information.');
}
}
function callResponse(message) {
if (message.response != 'accepted') {
console.info('Call not accepted by peer. Closing call');
stop();
setCallState(NO_CALL);
if (message.message) {
alert(message.message);
}
} else {
setCallState(IN_CALL);
webRtcPeer.processAnswer(message.sdpAnswer, function(error) {
if (error)
return console.error(error);
});
}
}
function startCommunication(message) {
setCallState(IN_CALL);
webRtcPeer.processAnswer(message.sdpAnswer, function(error) {
if (error)
return console.error(error);
});
}
function playResponse(message) {
if (message.response != 'accepted') {
hideSpinner(videoOutput);
document.getElementById('videoSmall').style.display = 'block';
alert(message.error);
document.getElementById('peer').focus();
setCallState(POST_CALL);
} else {
setCallState(IN_PLAY);
webRtcPeer.processAnswer(message.sdpAnswer, function(error) {
if (error)
return console.error(error);
});
}
}
function incomingCall(message) {
// If bussy just reject without disturbing user
if (callState != NO_CALL && callState != POST_CALL) {
var response = {
id : 'incomingCallResponse',
from : message.from,
callResponse : 'reject',
message : 'bussy'
};
return sendMessage(response);
}
setCallState(DISABLED);
if (confirm('User ' + message.from
+ ' is calling you. Do you accept the call?')) {
showSpinner(videoInput, videoOutput);
from = message.from;
var options = {
localVideo : videoInput,
remoteVideo : videoOutput,
onicecandidate : onIceCandidate
}
webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options,
function(error) {
if (error) {
return console.error(error);
}
this.generateOffer(onOfferIncomingCall);
});
} else {
var response = {
id : 'incomingCallResponse',
from : message.from,
callResponse : 'reject',
message : 'user declined'
};
sendMessage(response);
stop();
}
}
function onOfferIncomingCall(error, offerSdp) {
if (error)
return console.error('Error generating the offer ' + error);
var response = {
id : 'incomingCallResponse',
from : from,
callResponse : 'accept',
sdpOffer : offerSdp
};
sendMessage(response);
}
function register() {
var name = document.getElementById('name').value;
if (name == '') {
window.alert('You must insert your user name');
document.getElementById('name').focus();
return;
}
setRegisterState(REGISTERING);
var message = {
id : 'register',
name : name
};
sendMessage(message);
}
function call() {
if (document.getElementById('peer').value == '') {
document.getElementById('peer').focus();
window.alert('You must specify the peer name');
return;
}
setCallState(DISABLED);
showSpinner(videoInput, videoOutput);
var options = {
localVideo : videoInput,
remoteVideo : videoOutput,
onicecandidate : onIceCandidate
}
webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options,
function(error) {
if (error) {
return console.error(error);
}
this.generateOffer(onOfferCall);
});
}
function onOfferCall(error, offerSdp) {
if (error)
return console.error('Error generating the offer ' + error);
console.log('Invoking SDP offer callback function');
var message = {
id : 'call',
from : document.getElementById('name').value,
to : document.getElementById('peer').value,
sdpOffer : offerSdp
};
sendMessage(message);
}
function play() {
var peer = document.getElementById('peer').value;
if (peer == '') {
window
.alert("You must insert the name of the user recording to be played (field 'Peer')");
document.getElementById('peer').focus();
return;
}
document.getElementById('videoSmall').style.display = 'none';
setCallState(DISABLED);
showSpinner(videoOutput);
var options = {
remoteVideo : videoOutput,
onicecandidate : onIceCandidate
}
webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options,
function(error) {
if (error) {
return console.error(error);
}
this.generateOffer(onOfferPlay);
});
}
function onOfferPlay(error, offerSdp) {
console.log('Invoking SDP offer callback function');
var message = {
id : 'play',
user : document.getElementById('peer').value,
sdpOffer : offerSdp
};
sendMessage(message);
}
function playEnd() {
setCallState(POST_CALL);
hideSpinner(videoInput, videoOutput);
document.getElementById('videoSmall').style.display = 'block';
}
function stop(message) {
var stopMessageId = (callState == IN_CALL) ? 'stop' : 'stopPlay';
setCallState(POST_CALL);
if (webRtcPeer) {
webRtcPeer.dispose();
webRtcPeer = null;
if (!message) {
var message = {
id : stopMessageId
}
sendMessage(message);
}
}
hideSpinner(videoInput, videoOutput);
document.getElementById('videoSmall').style.display = 'block';
}
function sendMessage(message) {
var jsonMessage = JSON.stringify(message);
console.log('Senging message: ' + jsonMessage);
ws.send(jsonMessage);
}
function onIceCandidate(candidate) {
console.log('Local candidate ' + JSON.stringify(candidate));
var message = {
id : 'onIceCandidate',
candidate : candidate
};
sendMessage(message);
}
function showSpinner() {
for (var i = 0; i < arguments.length; i++) {
arguments[i].poster = './img/transparent-1px.png';
arguments[i].style.background = 'center transparent url("./img/spinner.gif") no-repeat';
}
}
function hideSpinner() {
for (var i = 0; i < arguments.length; i++) {
arguments[i].src = '';
arguments[i].poster = './img/webrtc.png';
arguments[i].style.background = '';
}
}
/**
* Lightbox utility (to display media pipeline image in a modal dialog)
*/
$(document).delegate('*[data-toggle="lightbox"]', 'click', function(event) {
event.preventDefault();
$(this).ekkoLightbox();
});
adapter.js
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/*
* Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree.
*/
/* eslint-env node */
'use strict';
var SDPUtils = require('sdp');
function fixStatsType(stat) {
return {
inboundrtp: 'inbound-rtp',
outboundrtp: 'outbound-rtp',
candidatepair: 'candidate-pair',
localcandidate: 'local-candidate',
remotecandidate: 'remote-candidate'
}[stat.type] || stat.type;
}
function writeMediaSection(transceiver, caps, type, stream, dtlsRole) {
var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
// Map ICE parameters (ufrag, pwd) to SDP.
sdp += SDPUtils.writeIceParameters(
transceiver.iceGatherer.getLocalParameters());
// Map DTLS parameters to SDP.
sdp += SDPUtils.writeDtlsParameters(
transceiver.dtlsTransport.getLocalParameters(),
type === 'offer' ? 'actpass' : dtlsRole || 'active');
sdp += 'a=mid:' + transceiver.mid + '\r\n';
if (transceiver.rtpSender && transceiver.rtpReceiver) {
sdp += 'a=sendrecv\r\n';
} else if (transceiver.rtpSender) {
sdp += 'a=sendonly\r\n';
} else if (transceiver.rtpReceiver) {
sdp += 'a=recvonly\r\n';
} else {
sdp += 'a=inactive\r\n';
}
if (transceiver.rtpSender) {
var trackId = transceiver.rtpSender._initialTrackId ||
transceiver.rtpSender.track.id;
transceiver.rtpSender._initialTrackId = trackId;
// spec.
var msid = 'msid:' + (stream ? stream.id : '-') + ' ' +
trackId + '\r\n';
sdp += 'a=' + msid;
// for Chrome. Legacy should no longer be required.
sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
' ' + msid;
// RTX
if (transceiver.sendEncodingParameters[0].rtx) {
sdp += 'a=ssrc:' + transceiver.sendEnco