Update React tutorial
@ -1,2 +0,0 @@
|
||||
**/node_modules
|
||||
**/build
|
||||
@ -1 +1,30 @@
|
||||
# openvidu-react
|
||||
# Basic React
|
||||
|
||||
Basic client application built with React. It internally uses [livekit-client-sdk-js](https://docs.livekit.io/client-sdk-js/).
|
||||
|
||||
For further information, check the [tutorial documentation](https://livekit-tutorials.openvidu.io/tutorials/application-client/react/).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Node](https://nodejs.org/en/download)
|
||||
|
||||
## Run
|
||||
|
||||
1. Download repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/OpenVidu/openvidu-livekit-tutorials.git
|
||||
cd openvidu-livekit-tutorials/application-client/openvidu-react
|
||||
```
|
||||
|
||||
2. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Run the application
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
FROM node:16-alpine3.16
|
||||
|
||||
COPY . ./openvidu-react
|
||||
|
||||
WORKDIR /openvidu-react
|
||||
|
||||
|
||||
# Install openvidu-react dependencies and build it
|
||||
RUN npm install && \
|
||||
npm run build && \
|
||||
cp -r ./build/ ./openvidu-basic-node/public
|
||||
|
||||
# Copy openvidu-basic-node
|
||||
RUN cp -r ./openvidu-basic-node /opt/openvidu-basic-node && \
|
||||
rm -rf ../openvidu-react
|
||||
|
||||
# Install openvidu-basic-node dependencies
|
||||
RUN npm --prefix /opt/openvidu-basic-node install
|
||||
|
||||
WORKDIR /opt/openvidu-basic-node
|
||||
|
||||
COPY docker/entrypoint.sh .
|
||||
|
||||
ENTRYPOINT [ "./entrypoint.sh" ]
|
||||
@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "No version argument provided. Usage: \"./create_image.sh <IMAGE_NAME>\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pushd ../
|
||||
|
||||
cp -r ../openvidu-basic-node .
|
||||
|
||||
trap 'rm -rf ./openvidu-basic-node' ERR
|
||||
|
||||
docker build --pull --no-cache --rm=true -f docker/Dockerfile -t "$1" .
|
||||
|
||||
rm -rf ./openvidu-basic-node
|
||||
@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec node index.js "$*"
|
||||
@ -1,77 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="src/assets/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- Bootstrap -->
|
||||
<script
|
||||
src="https://code.jquery.com/jquery-3.3.1.min.js"
|
||||
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
|
||||
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 -->
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Basic React</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
|
||||
<title>openvidu-react</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img
|
||||
class="demo-logo"
|
||||
src="src/assets/openvidu_vert_white_bg_trans_cropped.png"
|
||||
/>
|
||||
React</a
|
||||
>
|
||||
<a
|
||||
class="navbar-brand nav-icon"
|
||||
href="https://github.com/OpenVidu/openvidu-livekit-tutorials/tree/master/openvidu-react"
|
||||
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-react/"
|
||||
title="Documentation"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fa fa-book" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="root"></div>
|
||||
<!-- Bootstrap -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="text-muted">OpenVidu © 2023</div>
|
||||
<a href="http://www.openvidu.io/" target="_blank">
|
||||
<img
|
||||
class="openvidu-logo"
|
||||
src="src/assets/openvidu_globe_bg_transp_cropped.png"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
<!-- Font Awesome -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
||||
integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a href="/" title="Home"><h1>Basic React</h1></a>
|
||||
<div id="links">
|
||||
<a
|
||||
href="https://github.com/OpenVidu/openvidu-livekit-tutorials/tree/master/application-client/openvidu-js"
|
||||
title="GitHub Repository"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fa-brands fa-github"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://livekit-tutorials.openvidu.io/tutorials/application-client/javascript/"
|
||||
title="Documentation"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fa-solid fa-book"></i>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="root"></main>
|
||||
|
||||
<footer>
|
||||
<p class="text">Made with love by <span>OpenVidu Team</span></p>
|
||||
<a href="http://www.openvidu.io/" target="_blank">
|
||||
<img id="openvidu-logo" src="assets/images/openvidu_logo.png" alt="OpenVidu logo" />
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
events {
|
||||
worker_connections 512;
|
||||
}
|
||||
http {
|
||||
upstream openvidu-deployment {
|
||||
server host.docker.internal:4443;
|
||||
}
|
||||
upstream server-application {
|
||||
server host.docker.internal:5000;
|
||||
}
|
||||
upstream client-application {
|
||||
server host.docker.internal:3000;
|
||||
}
|
||||
server {
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/nginx/certs/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/key.pem;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_headers_hash_bucket_size 512;
|
||||
proxy_redirect off;
|
||||
|
||||
# Websockets
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# OpenVidu deployment API
|
||||
location /openvidu/api {
|
||||
proxy_pass http://openvidu-deployment;
|
||||
}
|
||||
|
||||
# OpenVidu WebSocket
|
||||
location ~ /openvidu$ {
|
||||
proxy_pass http://openvidu-deployment;
|
||||
}
|
||||
|
||||
# Server application requests
|
||||
location /api/ {
|
||||
proxy_pass http://server-application;
|
||||
}
|
||||
|
||||
# Client application requests
|
||||
location / {
|
||||
proxy_pass http://client-application;
|
||||
}
|
||||
}
|
||||
}
|
||||
3600
application-client/openvidu-react/package-lock.json
generated
@ -1,30 +1,29 @@
|
||||
{
|
||||
"name": "openvidu-react",
|
||||
"name": "basic-react",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5080 --host",
|
||||
"start": "vite --port 5080 --host",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"livekit-client": "^1.14.2",
|
||||
"livekit-client": "2.1.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"eslint": "^8.45.0",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5"
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
@ -1,76 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="shortcut icon" href="./resources/images/favicon.ico">
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
|
||||
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 -->
|
||||
<title>openvidu-react App</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img class="demo-logo" src="resources/images/openvidu_vert_white_bg_trans_cropped.png" /> React</a>
|
||||
<a class="navbar-brand nav-icon" href="https://github.com/OpenVidu/openvidu-tutorials/tree/master/openvidu-react" 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-react/" title="Documentation" target="_blank">
|
||||
<i class="fa fa-book" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="text-muted">OpenVidu © 2018</div>
|
||||
<a href="http://www.openvidu.io/" target="_blank">
|
||||
<img class="openvidu-logo" src="resources/images/openvidu_globe_bg_transp_cropped.png" />
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,47 +1,134 @@
|
||||
.stream-container {
|
||||
padding: 0;
|
||||
#join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
#join-dialog,
|
||||
|
||||
#join-dialog {
|
||||
width: 70%;
|
||||
max-width: 900px;
|
||||
padding: 60px;
|
||||
border-radius: 6px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
#join-dialog h2 {
|
||||
color: #4d4d4d;
|
||||
font-size: 60px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#join-dialog form {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#join-dialog label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
color: #0088aa;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
color: #0088aa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
color: #0088aa;
|
||||
border-color: #0088aa;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(0, 136, 170, 0.6);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(0, 136, 170, 0.6);
|
||||
}
|
||||
|
||||
#join-dialog button {
|
||||
display: block;
|
||||
margin: 20px auto 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #06d362;
|
||||
border-color: #06d362;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #1abd61;
|
||||
border-color: #1abd61;
|
||||
}
|
||||
|
||||
#room {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.participant-name {
|
||||
position: absolute;
|
||||
background: #f8f8f8;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
color: #777777;
|
||||
font-weight: bold;
|
||||
border-bottom-right-radius: 4px;
|
||||
z-index: 1000;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Estilos para el encabezado de la sala */
|
||||
#room-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Estilos para el título de la sala */
|
||||
#room-title {
|
||||
font-size: 24px;
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Contenedor de botones */
|
||||
#layout-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Media Queries */
|
||||
@media screen and (max-width: 768px) {
|
||||
#join-dialog {
|
||||
width: 90%;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
#join-dialog h2 {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
#layout-container {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
#join-dialog {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#join-dialog h2 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
#layout-container {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,303 +1,212 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import openviduCropped from './assets/openvidu_grey_bg_transp_cropped.png';
|
||||
import './App.css';
|
||||
import {
|
||||
LocalTrackPublication,
|
||||
RemoteTrack,
|
||||
RemoteTrackPublication,
|
||||
Room,
|
||||
RoomEvent,
|
||||
} from 'livekit-client';
|
||||
import axios from 'axios';
|
||||
import OvVideo from './OvVideo';
|
||||
import OvAudio from './OvAudio';
|
||||
LocalVideoTrack,
|
||||
RemoteParticipant,
|
||||
RemoteTrack,
|
||||
RemoteTrackPublication,
|
||||
Room,
|
||||
RoomEvent
|
||||
} from "livekit-client";
|
||||
import "./App.css";
|
||||
import { useState } from "react";
|
||||
import VideoComponent from "./components/VideoComponent";
|
||||
import AudioComponent from "./components/AudioComponent";
|
||||
|
||||
const App = () => {
|
||||
// For local development, leave these variables empty
|
||||
// For production, configure them with correct URLs depending on your deployment
|
||||
let APPLICATION_SERVER_URL = '';
|
||||
let LIVEKIT_URL = '';
|
||||
type TrackInfo = {
|
||||
trackPublication: RemoteTrackPublication;
|
||||
participantIdentity: string;
|
||||
};
|
||||
|
||||
// If APPLICATION_SERVER_URL is not configured, use default value from local development
|
||||
if (!APPLICATION_SERVER_URL) {
|
||||
if (window.location.hostname === 'localhost') {
|
||||
APPLICATION_SERVER_URL = 'http://localhost:6080/';
|
||||
} else {
|
||||
APPLICATION_SERVER_URL = 'https://' + window.location.hostname + ':6443/';
|
||||
}
|
||||
}
|
||||
// For local development, leave these variables empty
|
||||
// For production, configure them with correct URLs depending on your deployment
|
||||
let APPLICATION_SERVER_URL = "";
|
||||
let LIVEKIT_URL = "";
|
||||
configureUrls();
|
||||
|
||||
// If LIVEKIT_URL is not configured, use default value from local development
|
||||
if (!LIVEKIT_URL) {
|
||||
if (window.location.hostname === 'localhost') {
|
||||
LIVEKIT_URL = 'ws://localhost:7880/';
|
||||
} else {
|
||||
LIVEKIT_URL = 'wss://' + window.location.hostname + ':7443/';
|
||||
}
|
||||
}
|
||||
function configureUrls() {
|
||||
// If APPLICATION_SERVER_URL is not configured, use default value from local development
|
||||
if (!APPLICATION_SERVER_URL) {
|
||||
if (window.location.hostname === "localhost") {
|
||||
APPLICATION_SERVER_URL = "http://localhost:6080/";
|
||||
} else {
|
||||
APPLICATION_SERVER_URL = "https://" + window.location.hostname + ":6443/";
|
||||
}
|
||||
}
|
||||
|
||||
const [myRoomName, setMyRoomName] = useState('');
|
||||
const [myParticipantName, setMyParticipantName] = useState('');
|
||||
const [room, setRoom] = useState<Room | undefined>(undefined);
|
||||
const [myMainPublication, setMyMainPublication] = useState<
|
||||
LocalTrackPublication | RemoteTrackPublication | undefined
|
||||
>(undefined);
|
||||
const [localPublication, setLocalPublication] = useState<
|
||||
LocalTrackPublication | undefined
|
||||
>(undefined);
|
||||
const [remotePublications, setRemotePublications] = useState<
|
||||
RemoteTrackPublication[]
|
||||
>([]);
|
||||
// If LIVEKIT_URL is not configured, use default value from local development
|
||||
if (!LIVEKIT_URL) {
|
||||
if (window.location.hostname === "localhost") {
|
||||
LIVEKIT_URL = "ws://localhost:7880/";
|
||||
} else {
|
||||
LIVEKIT_URL = "wss://" + window.location.hostname + ":7443/";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const joinRoom = () => {
|
||||
function App() {
|
||||
const [room, setRoom] = useState<Room | undefined>(undefined);
|
||||
const [localTrack, setLocalTrack] = useState<LocalVideoTrack | undefined>(undefined);
|
||||
const [remoteTracks, setRemoteTracks] = useState<TrackInfo[]>([]);
|
||||
|
||||
// --- 1) Get a Room object ---
|
||||
const [participantName, setParticipantName] = useState("Participant" + Math.floor(Math.random() * 100));
|
||||
const [roomName, setRoomName] = useState("Test Room");
|
||||
|
||||
const room = new Room();
|
||||
setRoom(room);
|
||||
async function joinRoom() {
|
||||
// Initialize a new Room object
|
||||
const room = new Room();
|
||||
setRoom(room);
|
||||
|
||||
// --- 2) Specify the actions when events take place in the room ---
|
||||
// Specify the actions when events take place in the room
|
||||
// On every new Track received...
|
||||
room.on(
|
||||
RoomEvent.TrackSubscribed,
|
||||
(_track: RemoteTrack, publication: RemoteTrackPublication, participant: RemoteParticipant) => {
|
||||
setRemoteTracks((prev) => [
|
||||
...prev,
|
||||
{ trackPublication: publication, participantIdentity: participant.identity }
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
// On every new Track received...
|
||||
room.on(
|
||||
RoomEvent.TrackSubscribed,
|
||||
(_track: RemoteTrack, publication: RemoteTrackPublication) => {
|
||||
// Store the new publication in remotePublications array
|
||||
setRemotePublications((prevPublications) => [
|
||||
...prevPublications,
|
||||
publication,
|
||||
]);
|
||||
}
|
||||
);
|
||||
// On every Track destroyed...
|
||||
room.on(RoomEvent.TrackUnsubscribed, (_track: RemoteTrack, publication: RemoteTrackPublication) => {
|
||||
setRemoteTracks((prev) => prev.filter((track) => track.trackPublication.trackSid !== publication.trackSid));
|
||||
});
|
||||
|
||||
// On every track destroyed...
|
||||
room.on(
|
||||
RoomEvent.TrackUnsubscribed,
|
||||
(_track: RemoteTrack, publication: RemoteTrackPublication) => {
|
||||
// Remove the publication from 'remotePublications' array
|
||||
deleteRemoteTrackPublication(publication);
|
||||
}
|
||||
);
|
||||
try {
|
||||
// Get a token from your application server with the room name and participant name
|
||||
const token = await getToken(roomName, participantName);
|
||||
|
||||
getToken(myRoomName, myParticipantName).then(async (token: string) => {
|
||||
// First param is the LiveKit server URL. Second param is the access token
|
||||
try {
|
||||
await room.connect(LIVEKIT_URL, token);
|
||||
// --- 4) Publish your local tracks ---
|
||||
await room.localParticipant.setMicrophoneEnabled(true);
|
||||
const videoPublication = await room.localParticipant.setCameraEnabled(
|
||||
true
|
||||
);
|
||||
// Connect to the room with the LiveKit URL and the token
|
||||
await room.connect(LIVEKIT_URL, token);
|
||||
|
||||
// Set the main video in the page to display our webcam and store our localPublication
|
||||
setLocalPublication(videoPublication);
|
||||
setMyMainPublication(videoPublication);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'There was an error connecting to the room:',
|
||||
error.code,
|
||||
error.message
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
// Publish your camera and microphone
|
||||
await room.localParticipant.enableCameraAndMicrophone();
|
||||
setLocalTrack(room.localParticipant.videoTrackPublications.values().next().value.videoTrack);
|
||||
} catch (error) {
|
||||
console.log("There was an error connecting to the room:", (error as Error).message);
|
||||
await leaveRoom();
|
||||
}
|
||||
}
|
||||
|
||||
const leaveRoom = () => {
|
||||
// --- 5) Leave the room by calling 'disconnect' method over the Room object ---
|
||||
async function leaveRoom() {
|
||||
// Leave the room by calling 'disconnect' method over the Room object
|
||||
await room?.disconnect();
|
||||
|
||||
if (room) {
|
||||
room.disconnect();
|
||||
}
|
||||
// Reset the state
|
||||
setRoom(undefined);
|
||||
setLocalTrack(undefined);
|
||||
setRemoteTracks([]);
|
||||
}
|
||||
|
||||
// Empty all properties...
|
||||
setRemotePublications([]);
|
||||
setLocalPublication(undefined);
|
||||
setRoom(new Room());
|
||||
/**
|
||||
* --------------------------------------------
|
||||
* GETTING A TOKEN FROM YOUR APPLICATION SERVER
|
||||
* --------------------------------------------
|
||||
* The method below request the creation of a token to
|
||||
* your application server. This prevents the need to expose
|
||||
* your LiveKit API key and secret to the client side.
|
||||
*
|
||||
* In this sample code, there is no user control at all. Anybody could
|
||||
* access your application server endpoints. In a real production
|
||||
* environment, your application server must identify the user to allow
|
||||
* access to the endpoints.
|
||||
*/
|
||||
async function getToken(roomName: string, participantName: string) {
|
||||
const response = await fetch(APPLICATION_SERVER_URL + "token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
roomName: roomName,
|
||||
participantName: participantName
|
||||
})
|
||||
});
|
||||
|
||||
};
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(`Failed to get token: ${error.errorMessage}`);
|
||||
}
|
||||
|
||||
const deleteRemoteTrackPublication = useMemo(
|
||||
() => (publication: RemoteTrackPublication) => {
|
||||
setRemotePublications((prevPublications) =>
|
||||
prevPublications.filter((p) => p !== publication)
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const data = await response.json();
|
||||
return data.token;
|
||||
}
|
||||
|
||||
const handleMainVideoStream = (
|
||||
publication: LocalTrackPublication | RemoteTrackPublication
|
||||
) => {
|
||||
if (publication) {
|
||||
setMyMainPublication(publication);
|
||||
}
|
||||
};
|
||||
|
||||
const switchCamera = async () => {
|
||||
const localDevices: MediaDeviceInfo[] = await Room.getLocalDevices();
|
||||
const videoDevices = localDevices.filter(
|
||||
(device) => device.kind === 'videoinput'
|
||||
);
|
||||
const newDevice = videoDevices.find(
|
||||
(device) =>
|
||||
device.deviceId !==
|
||||
localPublication?.track?.mediaStreamTrack.getSettings().deviceId
|
||||
);
|
||||
if (!newDevice) return;
|
||||
room?.switchActiveDevice('videoinput', newDevice?.deviceId);
|
||||
};
|
||||
|
||||
/**
|
||||
* --------------------------------------------
|
||||
* GETTING A TOKEN FROM YOUR APPLICATION SERVER
|
||||
* --------------------------------------------
|
||||
* The methods below request the creation of a Token to
|
||||
* your application server. This keeps your OpenVidu deployment secure.
|
||||
*
|
||||
* In this sample code, there is no user control at all. Anybody could
|
||||
* access your application server endpoints! In a real production
|
||||
* environment, your application server must identify the user to allow
|
||||
* access to the endpoints.
|
||||
*
|
||||
* Visit https://docs.openvidu.io/en/stable/application-server to learn
|
||||
* more about the integration of OpenVidu in your application server.
|
||||
*/
|
||||
const getToken = useMemo(
|
||||
() =>
|
||||
async (roomName: string, participantName: string): Promise<string> => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
APPLICATION_SERVER_URL + 'token',
|
||||
{ roomName, participantName },
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
responseType: 'text',
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// Handle errors here
|
||||
console.error('Error getting token:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[APPLICATION_SERVER_URL]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{localPublication === undefined ? (
|
||||
<div id="join">
|
||||
<div id="img-div">
|
||||
<img src={openviduCropped} alt="OpenVidu logo" />
|
||||
</div>
|
||||
<div id="join-dialog" className="jumbotron vertical-center">
|
||||
<h1> Join a video room </h1>
|
||||
<form
|
||||
className="form-group"
|
||||
onSubmit={(e) => {
|
||||
joinRoom();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<label>Participant: </label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="userName"
|
||||
value={myParticipantName}
|
||||
onChange={(event) => setMyParticipantName(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<label> Room: </label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="roomName"
|
||||
value={myRoomName}
|
||||
onChange={(event) => setMyRoomName(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</p>
|
||||
<p className="text-center">
|
||||
<input
|
||||
className="btn btn-lg btn-success"
|
||||
name="commit"
|
||||
type="submit"
|
||||
value="JOIN"
|
||||
/>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div id="room">
|
||||
<div id="room-header">
|
||||
<h1 id="room-title">{myRoomName}</h1>
|
||||
<div className="button-container">
|
||||
<input
|
||||
className="btn btn-large btn-danger"
|
||||
type="button"
|
||||
id="buttonLeaveRoom"
|
||||
onClick={() => leaveRoom()}
|
||||
value="Leave room"
|
||||
/>
|
||||
<input
|
||||
className="btn btn-large btn-success"
|
||||
type="button"
|
||||
id="buttonSwitchCamera"
|
||||
onClick={() => switchCamera()}
|
||||
value="Switch Camera"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{myMainPublication !== undefined ? (
|
||||
<div id="main-video" className="col-md-6">
|
||||
{myMainPublication.videoTrack && (
|
||||
<OvVideo track={myMainPublication.videoTrack} />
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div id="video-container" className="col-md-6">
|
||||
{localPublication !== undefined ? (
|
||||
// sss
|
||||
<div className="stream-container col-md-6 col-xs-6">
|
||||
{localPublication.videoTrack && (
|
||||
<OvVideo
|
||||
onClick={() => handleMainVideoStream(localPublication)}
|
||||
track={localPublication.videoTrack}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{remotePublications.map((publication) => (
|
||||
<div
|
||||
key={publication.trackSid}
|
||||
className={`stream-container col-md-6 col-xs-6 ${
|
||||
publication.kind === 'audio' ? 'hidden' : ''
|
||||
}`}
|
||||
>
|
||||
{publication.videoTrack && (
|
||||
<OvVideo
|
||||
onClick={() => handleMainVideoStream(publication)}
|
||||
track={publication.videoTrack}
|
||||
/>
|
||||
)}
|
||||
{publication.audioTrack && (
|
||||
<OvAudio track={publication.audioTrack} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{!room ? (
|
||||
<div id="join">
|
||||
<div id="join-dialog">
|
||||
<h2>Join a Video Room</h2>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
joinRoom();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="participant-name">Participant</label>
|
||||
<input
|
||||
id="participant-name"
|
||||
className="form-control"
|
||||
type="text"
|
||||
value={participantName}
|
||||
onChange={(e) => setParticipantName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="room-name">Room</label>
|
||||
<input
|
||||
id="room-name"
|
||||
className="form-control"
|
||||
type="text"
|
||||
value={roomName}
|
||||
onChange={(e) => setRoomName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-lg btn-success"
|
||||
type="submit"
|
||||
disabled={!roomName || !participantName}
|
||||
>
|
||||
Join!
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div id="room">
|
||||
<div id="room-header">
|
||||
<h2 id="room-title">{roomName}</h2>
|
||||
<button className="btn btn-danger" id="leave-room-button" onClick={leaveRoom}>
|
||||
Leave Room
|
||||
</button>
|
||||
</div>
|
||||
<div id="layout-container">
|
||||
{localTrack && (
|
||||
<VideoComponent track={localTrack} participantIdentity={participantName} local={true} />
|
||||
)}
|
||||
{remoteTracks.map((remoteTrack) =>
|
||||
remoteTrack.trackPublication.kind === "video" ? (
|
||||
<VideoComponent
|
||||
key={remoteTrack.trackPublication.trackSid}
|
||||
track={remoteTrack.trackPublication.videoTrack!}
|
||||
participantIdentity={remoteTrack.participantIdentity}
|
||||
/>
|
||||
) : (
|
||||
<AudioComponent
|
||||
key={remoteTrack.trackPublication.trackSid}
|
||||
track={remoteTrack.trackPublication.audioTrack!}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client';
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
|
||||
interface OvAudioProps {
|
||||
track: LocalAudioTrack | RemoteAudioTrack;
|
||||
}
|
||||
|
||||
const OvAudio: FC<OvAudioProps> = ({ track }) => {
|
||||
const audioRef: React.MutableRefObject<null | HTMLAudioElement> =
|
||||
useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (audioRef.current) {
|
||||
track.attach(audioRef.current);
|
||||
}
|
||||
}, [track]);
|
||||
|
||||
return <audio ref={audioRef} playsInline autoPlay={true} />;
|
||||
};
|
||||
|
||||
export default OvAudio;
|
||||
@ -1,21 +0,0 @@
|
||||
import { LocalVideoTrack, RemoteVideoTrack } from 'livekit-client';
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
|
||||
interface OvVideoProps {
|
||||
track: LocalVideoTrack | RemoteVideoTrack;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const OvVideo: FC<OvVideoProps> = ({ track, onClick }) => {
|
||||
const videoRef: React.MutableRefObject<null | HTMLVideoElement> = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
track.attach(videoRef.current);
|
||||
}
|
||||
}, [track]);
|
||||
|
||||
return <video onClick={onClick} ref={videoRef} playsInline autoPlay={true} />;
|
||||
};
|
||||
|
||||
export default OvVideo;
|
||||
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
@ -0,0 +1,24 @@
|
||||
import { LocalAudioTrack, RemoteAudioTrack } from "livekit-client";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface AudioComponentProps {
|
||||
track: LocalAudioTrack | RemoteAudioTrack;
|
||||
}
|
||||
|
||||
function AudioComponent({ track }: AudioComponentProps) {
|
||||
const audioElement = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (audioElement.current) {
|
||||
track.attach(audioElement.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
track.detach();
|
||||
};
|
||||
}, [track]);
|
||||
|
||||
return <audio ref={audioElement} id={track.sid} />;
|
||||
}
|
||||
|
||||
export default AudioComponent;
|
||||
@ -0,0 +1,34 @@
|
||||
.video-container {
|
||||
position: relative;
|
||||
background: #3b3b3b;
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-container video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-container .participant-data {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.participant-data p {
|
||||
background: #f8f8f8;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
color: #777777;
|
||||
font-weight: bold;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
/* Media Queries */
|
||||
@media screen and (max-width: 480px) {
|
||||
.video-container {
|
||||
aspect-ratio: 9/16;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import { LocalVideoTrack, RemoteVideoTrack } from "livekit-client";
|
||||
import "./VideoComponent.css";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface VideoComponentProps {
|
||||
track: LocalVideoTrack | RemoteVideoTrack;
|
||||
participantIdentity: string;
|
||||
local?: boolean;
|
||||
}
|
||||
|
||||
function VideoComponent({ track, participantIdentity, local = false }: VideoComponentProps) {
|
||||
const videoElement = useRef<HTMLVideoElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoElement.current) {
|
||||
track.attach(videoElement.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
track.detach();
|
||||
};
|
||||
}, [track]);
|
||||
|
||||
return (
|
||||
<div id={"camera-" + participantIdentity} className="video-container">
|
||||
<div className="participant-data">
|
||||
<p>{participantIdentity + (local ? " (You)" : "")}</p>
|
||||
</div>
|
||||
<video ref={videoElement} id={track.sid}></video>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VideoComponent;
|
||||
@ -1,228 +1,110 @@
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
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;
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-top: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.navbar-header {
|
||||
width: 100%;
|
||||
header {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 30px;
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
padding: 5px 15px 5px 15px;
|
||||
float: right;
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #ccc !important;
|
||||
header a {
|
||||
color: #ccc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav i.fa {
|
||||
font-size: 40px;
|
||||
color: #ccc;
|
||||
header a:hover {
|
||||
color: #a9a9a9;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: #a9a9a9 !important;
|
||||
header i {
|
||||
padding: 5px 5px;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
nav i.fa:hover {
|
||||
color: #a9a9a9;
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
padding-bottom: 80px;
|
||||
footer {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
padding: 10px 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
position: absolute;
|
||||
top: 56%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
footer a {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.horizontal-center {
|
||||
margin: 0 auto;
|
||||
footer .text {
|
||||
color: #ccc;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
color: #0088aa;
|
||||
font-weight: bold;
|
||||
footer .text span {
|
||||
color: white;
|
||||
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);
|
||||
#openvidu-logo {
|
||||
height: 35px;
|
||||
-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;
|
||||
}
|
||||
|
||||
input.btn {
|
||||
font-weight: bold;
|
||||
#openvidu-logo:hover {
|
||||
-webkit-filter: grayscale(0.5);
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: bold !important;
|
||||
/* Media Queries */
|
||||
@media screen and (max-width: 768px) {
|
||||
header {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #06d362 !important;
|
||||
border-color: #06d362;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #1abd61 !important;
|
||||
border-color: #1abd61;
|
||||
}
|
||||
|
||||
.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-dialog h1 {
|
||||
color: #4d4d4d;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#img-div {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 19%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#img-div img {
|
||||
height: 15%;
|
||||
width: 42.5%;
|
||||
}
|
||||
|
||||
#join-dialog label {
|
||||
color: #0088aa;
|
||||
}
|
||||
|
||||
#join-dialog input.btn {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#session-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#session-title {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#video-container video {
|
||||
position: relative;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#video-container p {
|
||||
display: inline-block;
|
||||
background: #f8f8f8;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
color: #777777;
|
||||
font-weight: bold;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
/*video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}*/
|
||||
|
||||
#main-video p {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
background: #f8f8f8;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
left: 0;
|
||||
font-size: 22px;
|
||||
color: #777777;
|
||||
font-weight: bold;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
#main-video video {
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
|
||||
/* xs ans md screen resolutions*/
|
||||
|
||||
@media screen and (max-width: 991px) {
|
||||
.vertical-center {
|
||||
width: 85%;
|
||||
padding-top: 10px;
|
||||
top: 57%;
|
||||
}
|
||||
@media screen and (max-width: 480px) {
|
||||
header {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,8 @@
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||