openvidu-fault-tolerance

This commit is contained in:
pabloFuente 2022-05-12 14:40:50 +02:00
parent fdbdde57f2
commit b4104e092f
15 changed files with 14270 additions and 0 deletions

28
openvidu-fault-tolerance/.gitignore vendored Normal file
View 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

View 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.

View 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>

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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

View 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

View 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

View 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%;
}
}