openvidu-fault-tolerance
This commit is contained in:
parent
fdbdde57f2
commit
b4104e092f
28
openvidu-fault-tolerance/.gitignore
vendored
Normal file
28
openvidu-fault-tolerance/.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
target/*
|
||||
.settings
|
||||
.classpath
|
||||
.project
|
||||
53
openvidu-fault-tolerance/README.md
Normal file
53
openvidu-fault-tolerance/README.md
Normal file
@ -0,0 +1,53 @@
|
||||
# openvidu-fault-tolerance
|
||||
|
||||
This project exemplifies the fault tolerance capabilities of an application making use of an OpenVidu cluster, whether it is an OpenVidu Pro cluster or an OpenVidu Enterprise cluster. It demonstrates how to automatically rebuild any Session affected by a node crash, so final users do not have to perform any action to reconnect to a crashed Session.
|
||||
|
||||
## Compile and run the app
|
||||
|
||||
This is a SpringBoot application. Prerequisites:
|
||||
|
||||
| Dependency | Check version | Install |
|
||||
| ------------- | --------------- |---------------------------------------- |
|
||||
| Java 11 JDK | `java -version` | `sudo apt-get install -y openjdk-11-jdk` |
|
||||
| Maven | `mvn -v` | `sudo apt-get install -y maven` |
|
||||
|
||||
To compile and run the app:
|
||||
|
||||
```
|
||||
git clone git@github.com:OpenVidu/openvidu-tutorials.git
|
||||
cd openvidu-tutorials/openvidu-fault-tolerance
|
||||
mvn clean package
|
||||
java -jar target/openvidu-fault-tolerance*.jar --openvidu.url=OPENVIDU_PRO_DOMAIN --openvidu.secret=OPENVIDU_SECRET
|
||||
```
|
||||
|
||||
### Example
|
||||
- `OPENVIDU_PRO_DOMAIN` = `https://example-openvidu.io`
|
||||
- `OPENVIDU_SECRET` = `MY_SECRET`
|
||||
```
|
||||
git clone git@github.com:OpenVidu/openvidu-tutorials.git
|
||||
cd openvidu-tutorials/openvidu-fault-tolerance
|
||||
mvn clean package
|
||||
java -jar target/openvidu-fault-tolerance*.jar --openvidu.url=https://example-openvidu.io --openvidu.secret=MY_SECRET
|
||||
```
|
||||
|
||||
## Test the reconnection capabilities
|
||||
|
||||
### Media Node failure
|
||||
|
||||
A Session hosted in a Media Node suffering a crash will be automatically re-created and re-located in a different Media Node, without intervention of the final user. For this to work, the OpenVidu cluster must have at least 2 running Media Nodes. To test the reconnection capabilities of the application:
|
||||
|
||||
1. Make sure your OpenVidu cluster has at least 2 different Media Nodes.
|
||||
2. Connect 2 different users to the same session. They should both send and receive each other's video.
|
||||
3. Find out in which Media Node the session was located. You can call REST API method [GET Media Nodes](https://docs.openvidu.io/en/stable/reference-docs/REST-API/#get-all-medianodes) to do so.
|
||||
4. Terminate the Media Node hosting the session.
|
||||
5. After 3~4 seconds both users will automatically re-join the same session, successfully re-establishing the video streams.
|
||||
|
||||
### Master Node failure (OpenVidu Enterprise HA)
|
||||
|
||||
A session managed by a Master Node suffering a crash will be automatically re-created and re-located in a different Master Node, without intervention of the final user. For this to work, the OpenVidu Enterprise HA cluster must have at least 2 running Master Nodes. To test the reconnection capabilities of the application:
|
||||
|
||||
1. Make sure your OpenVidu cluster has at least 2 different Master Nodes.
|
||||
2. Connect 2 different users to the same session. They should both send and receive each other's video.
|
||||
3. Find out in which Master Node the session was located. To do so you will need to consume REST API method [GET Sessions](https://docs.openvidu.io/en/stable/reference-docs/REST-API/#reference-docs/REST-API/#get-all-sessions) directly from inside the Master Node machine. Connect to one of them and consume the REST API using directly openvidu-server-pro URI (`http://localhost:5443`), to skip the proxy that unifies the response from every Master Node. For example with cURL: `curl -X GET http://localhost:5443/openvidu/api/sessions -u OPENVIDUAPP:<YOUR_SECRET>`. The Master Node that returns a non-empty response is the one hosting the session.
|
||||
4. Terminate the Master Node hosting the session.
|
||||
5. Users will detect the crash and will rejoin the session automatically, successfully re-establishing the video streams. A very short amount of time will elapse from the detection of the crash and the re-joining to the session.
|
||||
51
openvidu-fault-tolerance/pom.xml
Normal file
51
openvidu-fault-tolerance/pom.xml
Normal file
@ -0,0 +1,51 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>io.openvidu</groupId>
|
||||
<artifactId>openvidu-fault-tolerance</artifactId>
|
||||
<version>2.21.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>openvidu-fault-tolerance</name>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.6.4</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
<start-class>io.openvidu.fault.tolerance.App</start-class>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.openvidu</groupId>
|
||||
<artifactId>openvidu-java-client</artifactId>
|
||||
<version>2.21.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
@ -0,0 +1,13 @@
|
||||
package io.openvidu.fault.tolerance;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class App
|
||||
{
|
||||
public static void main( String[] args )
|
||||
{
|
||||
SpringApplication.run(App.class, args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
package io.openvidu.fault.tolerance;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import io.openvidu.java.client.OpenVidu;
|
||||
import io.openvidu.java.client.OpenViduHttpException;
|
||||
import io.openvidu.java.client.OpenViduJavaClientException;
|
||||
import io.openvidu.java.client.Session;
|
||||
import io.openvidu.java.client.SessionProperties;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class MyRestController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MyRestController.class);
|
||||
|
||||
// URL where our OpenVidu server is listening
|
||||
private String OPENVIDU_URL;
|
||||
// Secret shared with our OpenVidu server
|
||||
private String SECRET;
|
||||
|
||||
// OpenVidu object as entrypoint of the SDK
|
||||
private OpenVidu openVidu;
|
||||
|
||||
public MyRestController(@Value("${openvidu.secret}") String secret, @Value("${openvidu.url}") String openviduUrl) {
|
||||
this.SECRET = secret;
|
||||
this.OPENVIDU_URL = openviduUrl;
|
||||
this.openVidu = new OpenVidu(OPENVIDU_URL, SECRET);
|
||||
log.info("Connecting to OpenVidu Pro Multi Master cluster at {}", OPENVIDU_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates a Connection for an existing or new Session, and returns
|
||||
* the Connection's token to the client side. It also handles the petition to
|
||||
* reconnect to a crashed session, as the process is exactly the same
|
||||
*/
|
||||
@RequestMapping(value = "/get-token", method = RequestMethod.POST)
|
||||
public ResponseEntity<?> getToken(@RequestBody Map<String, Object> params) {
|
||||
|
||||
log.info("Getting token | {sessionId}={}", params);
|
||||
|
||||
// The Session to connect
|
||||
String sessionId = (String) params.get("sessionId");
|
||||
|
||||
SessionProperties props = new SessionProperties.Builder().customSessionId(sessionId).build();
|
||||
Session session = null;
|
||||
try {
|
||||
session = this.openVidu.createSession(props);
|
||||
} catch (OpenViduHttpException e) {
|
||||
if (e.getStatus() == 502 || e.getStatus() == 503 || e.getStatus() == 504) {
|
||||
log.warn("The node handling the createSession operation is crashed ({}: {}). Retry", e.getStatus(),
|
||||
e.getMessage());
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e1) {
|
||||
}
|
||||
return getToken(params);
|
||||
} else {
|
||||
log.error("Unexpected error while creating session: {}", e.getMessage());
|
||||
return getErrorResponse(e);
|
||||
}
|
||||
} catch (OpenViduJavaClientException e) {
|
||||
log.error("Unexpected internal error while creating session. {}: {}", e.getClass().getCanonicalName(),
|
||||
e.getMessage());
|
||||
return getErrorResponse(e);
|
||||
}
|
||||
return returnToken(session);
|
||||
}
|
||||
|
||||
private ResponseEntity<?> returnToken(Session session) {
|
||||
try {
|
||||
String token = session.createConnection().getToken();
|
||||
|
||||
// Send the response with the token
|
||||
JsonObject responseJson = new JsonObject();
|
||||
responseJson.addProperty("token", token);
|
||||
return new ResponseEntity<>(responseJson, HttpStatus.OK);
|
||||
|
||||
} catch (OpenViduJavaClientException e1) {
|
||||
// If internal error generate an error message and return it to client
|
||||
log.error("Unexpected internal error while creating connection: {}", e1.getMessage());
|
||||
return getErrorResponse(e1);
|
||||
} catch (OpenViduHttpException e2) {
|
||||
if (404 == e2.getStatus()) {
|
||||
// The session wasn't found in OpenVidu Server
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (e2.getStatus() == 502 || e2.getStatus() == 503 || e2.getStatus() == 504) {
|
||||
log.warn("The node handling the createConnection operation is crashed ({}: {}). Retry", e2.getStatus(),
|
||||
e2.getMessage());
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e1) {
|
||||
}
|
||||
return returnToken(session);
|
||||
}
|
||||
return getErrorResponse(e2);
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<JsonObject> getErrorResponse(Exception e) {
|
||||
JsonObject json = new JsonObject();
|
||||
if (e.getCause() != null) {
|
||||
json.addProperty("cause", e.getCause().toString());
|
||||
}
|
||||
if (e.getStackTrace() != null) {
|
||||
json.addProperty("stacktrace", e.getStackTrace().toString());
|
||||
}
|
||||
json.addProperty("error", e.getMessage());
|
||||
json.addProperty("exception", e.getClass().getCanonicalName());
|
||||
return new ResponseEntity<>(json, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
server.port: 5000
|
||||
server.ssl.enabled: true
|
||||
server.ssl.key-store: classpath:openvidu-selfsigned.jks
|
||||
server.ssl.key-store-password: openvidu
|
||||
server.ssl.key-store-type: JKS
|
||||
server.ssl.key-alias: openvidu-selfsigned
|
||||
spring.mvc.converters.preferred-json-mapper: gson
|
||||
|
||||
openvidu.url: https://localhost:4443/
|
||||
openvidu.secret: MY_SECRET
|
||||
Binary file not shown.
245
openvidu-fault-tolerance/src/main/resources/static/app.js
Normal file
245
openvidu-fault-tolerance/src/main/resources/static/app.js
Normal file
@ -0,0 +1,245 @@
|
||||
var OV;
|
||||
var session;
|
||||
|
||||
var sessionId;
|
||||
var numVideos;
|
||||
|
||||
|
||||
/* OPENVIDU METHODS */
|
||||
|
||||
async function joinSession() {
|
||||
let token = await getToken();
|
||||
await connectToSessionWithToken(token);
|
||||
}
|
||||
|
||||
function leaveSession() {
|
||||
session.disconnect();
|
||||
}
|
||||
|
||||
async function connectToSessionWithToken(token) {
|
||||
|
||||
numVideos = 0;
|
||||
|
||||
OV = new OpenVidu();
|
||||
|
||||
session = OV.initSession();
|
||||
|
||||
session.on('connectionCreated', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
session.on('connectionDestroyed', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
session.on('streamCreated', event => {
|
||||
pushEvent(event);
|
||||
|
||||
// Subscribe to the Stream to receive it
|
||||
// HTML video will be appended to element with 'video-container' id
|
||||
var subscriber = session.subscribe(event.stream, 'video-container');
|
||||
|
||||
// When the HTML video has been appended to DOM...
|
||||
subscriber.on('videoElementCreated', event => {
|
||||
pushEvent(event);
|
||||
// Add a new HTML element for the user's name and nickname over its video
|
||||
updateNumVideos(1);
|
||||
});
|
||||
|
||||
// When the HTML video has been appended to DOM...
|
||||
subscriber.on('videoElementDestroyed', event => {
|
||||
pushEvent(event);
|
||||
// Add a new HTML element for the user's name and nickname over its video
|
||||
updateNumVideos(-1);
|
||||
});
|
||||
|
||||
// When the subscriber stream has started playing media...
|
||||
subscriber.on('streamPlaying', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
});
|
||||
|
||||
session.on('streamDestroyed', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
session.on('sessionDisconnected', event => {
|
||||
pushEvent(event);
|
||||
session = null;
|
||||
numVideos = 0;
|
||||
if (event.reason === 'nodeCrashed') {
|
||||
console.warn('Node has crashed!');
|
||||
$('#reconnectionModal').modal('show');
|
||||
joinSession();
|
||||
} else {
|
||||
$('#join').show();
|
||||
$('#session').hide();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
await session.connect(token);
|
||||
|
||||
// Set page layout for active call
|
||||
|
||||
$('#session-title').text(sessionId);
|
||||
$('#join').hide();
|
||||
$('#session').show();
|
||||
$('#reconnectionModal').modal('hide');
|
||||
|
||||
// Get your own camera stream
|
||||
|
||||
var publisher = OV.initPublisher('video-container', {
|
||||
audioSource: undefined, // The source of audio. If undefined default microphone
|
||||
videoSource: undefined, // The source of video. If undefined default webcam
|
||||
publishAudio: true, // Whether you want to start publishing with your audio unmuted or not
|
||||
publishVideo: true, // Whether you want to start publishing with your video enabled or not
|
||||
resolution: '640x480', // The resolution of your video
|
||||
frameRate: 30, // The frame rate of your video
|
||||
insertMode: 'APPEND', // How the video is inserted in the target element 'video-container'
|
||||
mirror: false // Whether to mirror your local video or not
|
||||
});
|
||||
|
||||
// Specify the actions when events take place in our publisher
|
||||
|
||||
// When the publisher stream has started playing media...
|
||||
publisher.on('accessAllowed', event => {
|
||||
pushEvent({
|
||||
type: 'accessAllowed'
|
||||
});
|
||||
});
|
||||
|
||||
publisher.on('accessDenied', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
publisher.on('accessDialogOpened', event => {
|
||||
pushEvent({
|
||||
type: 'accessDialogOpened'
|
||||
});
|
||||
});
|
||||
|
||||
publisher.on('accessDialogClosed', event => {
|
||||
pushEvent({
|
||||
type: 'accessDialogClosed'
|
||||
});
|
||||
});
|
||||
|
||||
// When the publisher stream has started playing media...
|
||||
publisher.on('streamCreated', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
// When our HTML video has been added to DOM...
|
||||
publisher.on('videoElementCreated', event => {
|
||||
pushEvent(event);
|
||||
updateNumVideos(1);
|
||||
$(event.element).prop('muted', true); // Mute local video
|
||||
});
|
||||
|
||||
// When the HTML video has been appended to DOM...
|
||||
publisher.on('videoElementDestroyed', event => {
|
||||
pushEvent(event);
|
||||
// Add a new HTML element for the user's name and nickname over its video
|
||||
updateNumVideos(-1);
|
||||
});
|
||||
|
||||
// When the publisher stream has started playing media...
|
||||
publisher.on('streamPlaying', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
// Publish your stream
|
||||
|
||||
session.publish(publisher);
|
||||
|
||||
} catch (error) {
|
||||
console.warn('There was an error connecting to the session:', error.code, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/* OPENVIDU METHODS */
|
||||
|
||||
|
||||
|
||||
/* APPLICATION REST METHODS */
|
||||
|
||||
async function getToken() {
|
||||
|
||||
sessionId = $("#sessionId").val(); // Video-call chosen by the user
|
||||
var mustRetry = true;
|
||||
|
||||
while (mustRetry) {
|
||||
try {
|
||||
const result = await $.ajax({
|
||||
url: 'api/get-token',
|
||||
type: 'POST',
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({ sessionId })
|
||||
});
|
||||
console.log('Request of TOKEN gone WELL (TOKEN:' + result + ')');
|
||||
mustRetry = false;
|
||||
return result.token;
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.warn('The session was closed. Try again');
|
||||
} else {
|
||||
mustRetry = false;
|
||||
console.error('Unexpected error', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* APPLICATION REST METHODS */
|
||||
|
||||
|
||||
|
||||
/* APPLICATION BROWSER METHODS */
|
||||
|
||||
events = '';
|
||||
|
||||
window.onbeforeunload = function () { // Gracefully leave session
|
||||
if (session) {
|
||||
leaveSession();
|
||||
}
|
||||
}
|
||||
|
||||
function updateNumVideos(i) {
|
||||
numVideos += i;
|
||||
$('video').removeClass();
|
||||
switch (numVideos) {
|
||||
case 1:
|
||||
$('video').addClass('two');
|
||||
break;
|
||||
case 2:
|
||||
$('video').addClass('two');
|
||||
break;
|
||||
case 3:
|
||||
$('video').addClass('three');
|
||||
break;
|
||||
case 4:
|
||||
$('video').addClass('four');
|
||||
break;
|
||||
case 5:
|
||||
$('video').addClass('five');
|
||||
break;
|
||||
case 6:
|
||||
$('video').addClass('six');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function pushEvent(event) {
|
||||
events += (!events ? '' : '\n') + event.type;
|
||||
$('#textarea-events').text(events);
|
||||
}
|
||||
|
||||
function clearEventsTextarea() {
|
||||
$('#textarea-events').text('');
|
||||
events = '';
|
||||
}
|
||||
|
||||
/* APPLICATION BROWSER METHODS */
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
121
openvidu-fault-tolerance/src/main/resources/static/index.html
Normal file
121
openvidu-fault-tolerance/src/main/resources/static/index.html
Normal file
@ -0,0 +1,121 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>openvidu-fault-tolerance</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" charset="utf-8">
|
||||
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
|
||||
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
|
||||
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous">
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<!-- Bootstrap -->
|
||||
|
||||
<link rel="styleSheet" href="style.css" type="text/css" media="screen">
|
||||
<script src="openvidu-browser-2.21.0.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip({
|
||||
html: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img class="demo-logo"
|
||||
src="images/openvidu_vert_white_bg_trans_cropped.png" />openvidu-fault-tolerance</a>
|
||||
<a class="navbar-brand nav-icon"
|
||||
href="https://github.com/OpenVidu/openvidu-tutorials/tree/master/openvidu-recording-java"
|
||||
title="GitHub Repository" target="_blank">
|
||||
<i class="fa fa-github" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a class="navbar-brand nav-icon"
|
||||
href="http://www.docs.openvidu.io/en/stable/tutorials/openvidu-js-java/" title="Documentation"
|
||||
target="_blank">
|
||||
<i class="fa fa-book" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="main-container" class="container">
|
||||
<div id="join" class="vertical-center">
|
||||
<div id="img-div">
|
||||
<img src="images/openvidu_grey_bg_transp_cropped.png" />
|
||||
</div>
|
||||
<div id="join-dialog" class="jumbotron">
|
||||
<h1>Join a video session</h1>
|
||||
<form class="form-group" onsubmit="return false">
|
||||
<p>
|
||||
<label>Session</label>
|
||||
<input class="form-control" type="text" id="sessionId" value="SessionA" required>
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<button class="btn btn-lg btn-success" id="join-btn" onclick="joinSession()">Join!</button>
|
||||
</p>
|
||||
</form>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="session" style="display: none">
|
||||
<div id="session-header">
|
||||
<h1 id="session-title"></h1>
|
||||
<input class="btn btn-sm btn-danger" type="button" id="buttonLeaveSession" onmouseup="leaveSession()"
|
||||
value="Leave session">
|
||||
</div>
|
||||
<div id="video-container" class="col-md-12"></div>
|
||||
<div id="recording-btns">
|
||||
<div class="textarea-container" id="textarea-events-container">
|
||||
<button type="button" class="btn btn-outline-secondary" id="clear-events-btn"
|
||||
onclick="clearEventsTextarea()">Clear</button>
|
||||
<span>OpenVidu events</span>
|
||||
<textarea id="textarea-events" readonly="true" class="form-control"
|
||||
name="textarea-events"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="text-muted">OpenVidu © 2017</div>
|
||||
<a href="http://www.openvidu.io/" target="_blank">
|
||||
<img class="openvidu-logo" src="images/openvidu_globe_bg_transp_cropped.png" />
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="modal fade" id="reconnectionModal" tabindex="-1" role="dialog" aria-labelledby="reconnectionModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="reconnectionModalLabel">Oops! Some problem detected. Reconnecting to the
|
||||
session...</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
483
openvidu-fault-tolerance/src/main/resources/static/style.css
Normal file
483
openvidu-fault-tolerance/src/main/resources/static/style.css
Normal file
@ -0,0 +1,483 @@
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
nav {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
background-color: #4d4d4d !important;
|
||||
border-color: #4d4d4d !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
|
||||
.navbar-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
padding: 5px 15px 5px 15px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #ccc !important;
|
||||
}
|
||||
|
||||
nav i.fa {
|
||||
font-size: 40px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: #a9a9a9 !important;
|
||||
}
|
||||
|
||||
nav i.fa:hover {
|
||||
color: #a9a9a9;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
padding-bottom: 80px;
|
||||
height: 100%;
|
||||
margin-top: -70px;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
width: -webkit-fit-content;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.vertical-center#not-logged form {
|
||||
width: -moz-fit-content;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.vertical-center#not-logged table {
|
||||
width: -moz-fit-content;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.vertical-center table {
|
||||
margin-top: 3em !important;
|
||||
}
|
||||
|
||||
.horizontal-center {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
color: #0088aa;
|
||||
font-weight: bold;
|
||||
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #0088aa;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(0, 136, 170, 0.6);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(0, 136, 170, 0.6);
|
||||
}
|
||||
|
||||
input.btn {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #06d362 !important;
|
||||
border-color: #06d362;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #1abd61 !important;
|
||||
border-color: #1abd61;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background-color: #0088aa !important;
|
||||
border-color: #0088aa;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background-color: #00708c !important;
|
||||
border-color: #00708c;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #ffcc00 !important;
|
||||
border-color: #ffcc00;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background-color: #eabb3a !important;
|
||||
border-color: #eabb3a;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.btn-warning:active {
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.btn-warning:focus {
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.btn-warning:active:focus {
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
|
||||
.footer .text-muted {
|
||||
margin: 20px 0;
|
||||
float: left;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.openvidu-logo {
|
||||
height: 35px;
|
||||
float: right;
|
||||
margin: 12px 0;
|
||||
-webkit-transition: all 0.1s ease-in-out;
|
||||
-moz-transition: all 0.1s ease-in-out;
|
||||
-o-transition: all 0.1s ease-in-out;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.openvidu-logo:hover {
|
||||
-webkit-filter: grayscale(0.5);
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
.demo-logo {
|
||||
margin: 0;
|
||||
height: 22px;
|
||||
float: left;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
a:hover .demo-logo {
|
||||
-webkit-filter: brightness(0.7);
|
||||
filter: brightness(0.7);
|
||||
}
|
||||
|
||||
#join {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
#not-logged {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
#join-dialog h1 {
|
||||
color: #4d4d4d;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#join-dialog label {
|
||||
color: #0088aa;
|
||||
}
|
||||
|
||||
#join-dialog input.btn {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#join-dialog hr {
|
||||
background: #4d4d4d;
|
||||
}
|
||||
|
||||
#img-div {
|
||||
text-align: center;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
|
||||
#img-div img {
|
||||
height: 15%;
|
||||
}
|
||||
|
||||
#logged {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#join {
|
||||
max-width: 700px;
|
||||
margin: auto;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
#session-header {
|
||||
margin-bottom: 20px;
|
||||
height: 8%;
|
||||
margin-top: 70px;
|
||||
}
|
||||
|
||||
#session-header form {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#session-header input.btn {
|
||||
float: right;
|
||||
margin-top: 20px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#session-title {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#session-header .form-control {
|
||||
width: initial;
|
||||
float: right;
|
||||
margin: 18px 0px 0px 5px;
|
||||
}
|
||||
|
||||
#video-container {
|
||||
width: 100%;
|
||||
max-height: 42%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#video-container video.two {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
#video-container video.three {
|
||||
max-width: 33.33%;
|
||||
}
|
||||
|
||||
#video-container video.four {
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
#video-container video.five {
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
#video-container video.six {
|
||||
max-width: 16.66%;
|
||||
}
|
||||
|
||||
#video-container div {
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
margin-left: calc(-50% + 15px);
|
||||
}
|
||||
|
||||
#video-container p {
|
||||
display: inline-block;
|
||||
background: #f8f8f8;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
color: #777777;
|
||||
font-weight: bold;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
#video-container p.userName {
|
||||
float: right;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 0px;
|
||||
font-weight: lighter;
|
||||
font-size: 12px;
|
||||
background: #777777;
|
||||
color: #f8f8f8;
|
||||
}
|
||||
|
||||
video {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
#session {
|
||||
height: 100%;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
#session img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: inline-block;
|
||||
object-fit: contain;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
#session #video-container img {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 50%;
|
||||
cursor: pointer;
|
||||
object-fit: cover;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
table i {
|
||||
cursor: pointer;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
#tooltip-div {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#tooltip-div hr {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
#login-info {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#login-info form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#login-info div {
|
||||
display: inline;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#name-user {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#recording-btns {
|
||||
display: inline-block;
|
||||
padding-left: 15px;
|
||||
padding-top: 20px;
|
||||
width: 100%;
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
#recording-btns .btns {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#recording-btns .btns .form-control {
|
||||
width: initial;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#recording-btns .btns form {
|
||||
display: inline;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#recording-btns textarea {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.textarea-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 85%;
|
||||
margin-top: 20px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-size: 13px !important;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#textarea-events-container {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.textarea-container button {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.textarea-container span {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
right: 1px;
|
||||
padding: 3px;
|
||||
border-bottom-right-radius: 4px;
|
||||
z-index: 1;
|
||||
color: #a5a5a5;
|
||||
background-color: #ededee;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.textarea-container textarea {
|
||||
height: 100%;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.vertical-separator-bottom {
|
||||
width: 2px;
|
||||
height: 34px;
|
||||
display: inline-block;
|
||||
background-color: #cbcbcb;
|
||||
margin: 0 8px 0 8px;
|
||||
margin-bottom: -12px;
|
||||
}
|
||||
|
||||
.vertical-separator-top {
|
||||
width: 2px;
|
||||
height: 30px;
|
||||
background-color: #cbcbcb;
|
||||
margin: 20px 8px 0 15px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* xs ans md screen resolutions*/
|
||||
|
||||
@media screen and (max-width: 991px) {
|
||||
#join {
|
||||
padding-top: inherit;
|
||||
}
|
||||
|
||||
#not-logged {
|
||||
padding-top: inherit;
|
||||
}
|
||||
|
||||
.container .navbar-header {
|
||||
margin-right: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
padding: 9px 8px 9px 8px;
|
||||
}
|
||||
|
||||
nav i.fa {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#img-div {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
#img-div img {
|
||||
height: 10%;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user