diff --git a/basic/frontend/javascript/README.md b/basic/frontend/javascript/README.md new file mode 100644 index 00000000..270d66eb --- /dev/null +++ b/basic/frontend/javascript/README.md @@ -0,0 +1,32 @@ +# Basic Frontend JavaScript + +Basic client application built with plain HTML, CSS and JavaScript. 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/basic/frontend/javascript). + +## 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/basic/frontend/javascript +``` + +2. Run the application + +To run this tutorial, you will need a HTTP web server installed on your development computer. If you have Node.js installed, you can easily set up [http-server](https://github.com/indexzero/http-server): + +```bash +npm install --location=global http-server +``` + +After installing http-server, serve the tutorial: + +```bash +http-server -p 5080 web +``` diff --git a/basic/frontend/javascript/web/app.js b/basic/frontend/javascript/web/app.js new file mode 100644 index 00000000..9e34733c --- /dev/null +++ b/basic/frontend/javascript/web/app.js @@ -0,0 +1,180 @@ +// For local development, leave these variables empty +// For production, configure them with correct URLs depending on your deployment +var APPLICATION_SERVER_URL = ""; +var LIVEKIT_URL = ""; +configureUrls(); + +var LivekitClient = window.LivekitClient; +var room; + +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/"; + } + } + + // 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 joinRoom() { + var myRoomName = document.getElementById("roomName").value; + var myUserName = document.getElementById("userName").value; + + // --- 1) Get a Room object --- + room = new LivekitClient.Room(); + + // --- 2) Specify the actions when events take place in the room --- + // On every new Track received... + room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, publication, participant) => { + const element = track.attach(); + element.id = track.sid; + element.className = "removable"; + document.getElementById("video-container").appendChild(element); + + if (track.kind === "video") { + appendUserData(element, participant.identity); + } + }); + + // On every new Track destroyed... + room.on(LivekitClient.RoomEvent.TrackUnsubscribed, (track, publication, participant) => { + track.detach(); + document.getElementById(track.sid)?.remove(); + + if (track.kind === "video") { + removeUserData(participant); + } + }); + + // --- 3) Connect to the room with a valid access token --- + // Get a token from the application backend + getToken(myRoomName, myUserName).then((token) => { + // First param is the LiveKit server URL. Second param is the access token + room.connect(LIVEKIT_URL, token) + .then(() => { + // --- 4) Set page layout for active call --- + document.getElementById("room-title").innerText = myRoomName; + document.getElementById("join").style.display = "none"; + document.getElementById("room").style.display = "block"; + + // --- 5) Publish your local tracks --- + room.localParticipant.setMicrophoneEnabled(true); + room.localParticipant.setCameraEnabled(true).then((publication) => { + const element = publication.track.attach(); + document.getElementById("video-container").appendChild(element); + initMainVideo(element, myUserName); + appendUserData(element, myUserName); + element.className = "removable"; + }); + }) + .catch((error) => { + console.log("There was an error connecting to the room:", error.code, error.message); + }); + }); +} + +function leaveRoom() { + // --- 6) Leave the room by calling 'disconnect' method over the Room object --- + room.disconnect(); + + // Removing all HTML elements with user's nicknames. + // HTML videos are automatically removed when leaving a Room + removeAllUserData(); + + // Back to 'Join room' page + document.getElementById("join").style.display = "block"; + document.getElementById("room").style.display = "none"; +} + +window.onbeforeunload = function () { + if (room) room.disconnect(); +}; + +// APPLICATION SPECIFIC METHODS +window.addEventListener("load", function () { + generateParticipantInfo(); +}); + +function generateParticipantInfo() { + document.getElementById("roomName").value = "RoomA"; + document.getElementById("userName").value = "Participant" + Math.floor(Math.random() * 100); +} + +function appendUserData(videoElement, participantIdentity) { + var dataNode = document.createElement("div"); + dataNode.className = "removable"; + dataNode.id = "data-" + participantIdentity; + dataNode.innerHTML = "

" + participantIdentity + "

"; + videoElement.parentNode.insertBefore(dataNode, videoElement.nextSibling); + addClickListener(videoElement, participantIdentity); +} + +function removeUserData(participant) { + var dataNode = document.getElementById("data-" + participant.identity); + dataNode?.parentNode.removeChild(dataNode); +} + +function removeAllUserData() { + var elementsToRemove = document.getElementsByClassName("removable"); + while (elementsToRemove[0]) { + elementsToRemove[0].parentNode.removeChild(elementsToRemove[0]); + } +} + +function addClickListener(videoElement, userData) { + videoElement.addEventListener("click", function () { + var mainVideo = $("#main-video video").get(0); + if (mainVideo.srcObject !== videoElement.srcObject) { + $("#main-video").fadeOut("fast", () => { + $("#main-video p").html(userData); + mainVideo.srcObject = videoElement.srcObject; + $("#main-video").fadeIn("fast"); + }); + } + }); +} + +function initMainVideo(videoElement, userData) { + document.querySelector("#main-video video").srcObject = videoElement.srcObject; + document.querySelector("#main-video p").innerHTML = userData; + document.querySelector("#main-video video")["muted"] = true; +} + +/** + * -------------------------------------------- + * 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. + */ +function getToken(roomName, participantName) { + return new Promise((resolve, reject) => { + $.ajax({ + type: "POST", + url: APPLICATION_SERVER_URL + "token", + data: JSON.stringify({ + roomName, + participantName, + }), + headers: { "Content-Type": "application/json" }, + success: (token) => resolve(token), + error: (error) => reject(error), + }); + }); +} diff --git a/basic/frontend/javascript/web/index.html b/basic/frontend/javascript/web/index.html new file mode 100644 index 00000000..36a557f1 --- /dev/null +++ b/basic/frontend/javascript/web/index.html @@ -0,0 +1,114 @@ + + + openvidu-js + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+

Join a video room

+
+

+ + +

+

+ + +

+

+ +

+
+
+
+ + +
+ + + + diff --git a/basic/frontend/javascript/web/resources/images/favicon.ico b/basic/frontend/javascript/web/resources/images/favicon.ico new file mode 100644 index 00000000..0e2249ad Binary files /dev/null and b/basic/frontend/javascript/web/resources/images/favicon.ico differ diff --git a/basic/frontend/javascript/web/resources/images/openvidu_globe_bg_transp_cropped.png b/basic/frontend/javascript/web/resources/images/openvidu_globe_bg_transp_cropped.png new file mode 100644 index 00000000..e0309e62 Binary files /dev/null and b/basic/frontend/javascript/web/resources/images/openvidu_globe_bg_transp_cropped.png differ diff --git a/basic/frontend/javascript/web/resources/images/openvidu_grey_bg_transp_cropped.png b/basic/frontend/javascript/web/resources/images/openvidu_grey_bg_transp_cropped.png new file mode 100644 index 00000000..49f311a6 Binary files /dev/null and b/basic/frontend/javascript/web/resources/images/openvidu_grey_bg_transp_cropped.png differ diff --git a/basic/frontend/javascript/web/resources/images/openvidu_vert_white_bg_trans_cropped.png b/basic/frontend/javascript/web/resources/images/openvidu_vert_white_bg_trans_cropped.png new file mode 100644 index 00000000..a1c8b2d7 Binary files /dev/null and b/basic/frontend/javascript/web/resources/images/openvidu_vert_white_bg_trans_cropped.png differ diff --git a/basic/frontend/javascript/web/style.css b/basic/frontend/javascript/web/style.css new file mode 100644 index 00000000..488cb987 --- /dev/null +++ b/basic/frontend/javascript/web/style.css @@ -0,0 +1,269 @@ +html { + position: relative; + 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; +} + +/*vertical-center { + position: relative; + top: 30%; + left: 50%; + transform: translate(-50%, -50%); +}*/ + +.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, 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); +} + +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; +} + +.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 { + margin-left: auto; + margin-right: auto; + max-width: 70%; +} + +#join-dialog h1 { + color: #4d4d4d; + font-weight: bold; + text-align: center; +} + +#img-div { + text-align: center; + margin-top: 3em; + margin-bottom: 3em; + /*position: relative; + top: 20%; + left: 50%; + transform: translate(-50%, -50%);*/ +} + +#img-div img { + height: 15%; +} + +#join-dialog label { + color: #0088aa; +} + +#join-dialog input.btn { + margin-top: 15px; +} + +#room-header { + margin-bottom: 20px; +} + +#room-title { + display: inline-block; +} + +#buttonLeaveRoom { + float: right; + margin-top: 20px; +} + +#video-container video { + position: relative; + float: left; + width: 50%; + cursor: pointer; +} + +#video-container div { + float: left; + width: 50%; + position: relative; + margin-left: -50%; +} + +#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 { + width: 100%; + height: auto; +} + +#main-video p { + position: absolute; + display: inline-block; + background: #f8f8f8; + padding-left: 5px; + padding-right: 5px; + font-size: 22px; + color: #777777; + font-weight: bold; + border-bottom-right-radius: 4px; +} + +#room img { + width: 100%; + height: auto; + display: inline-block; + object-fit: contain; + vertical-align: baseline; +} + +#room #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) and (orientation: portrait) { + #join-dialog { + max-width: inherit; + } + #img-div img { + height: 10%; + } + #img-div { + margin-top: 2em; + margin-bottom: 2em; + } + .container-fluid > .navbar-collapse, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container > .navbar-header { + margin-right: 0; + margin-left: 0; + } + .navbar-header i.fa { + font-size: 30px; + } + .navbar-header a.nav-icon { + padding: 7px 3px 7px 3px; + } +} + +@media only screen and (max-height: 767px) and (orientation: landscape) { + #img-div { + margin-top: 1em; + margin-bottom: 1em; + } + #join-dialog { + max-width: inherit; + } +}