diff --git a/openvidu-js-screen-share/web/app.js b/openvidu-js-screen-share/web/app.js index ec54eb66..c3c3d08f 100644 --- a/openvidu-js-screen-share/web/app.js +++ b/openvidu-js-screen-share/web/app.js @@ -1,186 +1,149 @@ -// OpenVidu global variables -var OVCamera; -var OVScreen -var sessionCamera; -var sessionScreen - -// User name and session name global variables +var LivekitClient = window.LivekitClient; +var room; var myUserName; -var mySessionId; -var screensharing = false; - +var myRoomName; +var isScreenShared = false; +var screenSharePublication; /* OPENVIDU METHODS */ -function joinSession() { +function joinRoom() { + myRoomName = document.getElementById('roomName').value; + myUserName = document.getElementById('userName').value; - mySessionId = document.getElementById("sessionId").value; - myUserName = document.getElementById("userName").value; + // --- 1) Get a Room object --- - // --- 1) Create two OpenVidu objects. + room = new LivekitClient.Room(); - // 'OVCamera' will handle Camera operations. - // 'OVScreen' will handle screen sharing operations - OVCamera = new OpenVidu(); - OVScreen = new OpenVidu(); + // --- 2) Specify the actions when events take place in the room --- - // --- 2) Init two OpenVidu Session Objects --- - - // 'sessionCamera' will handle camera operations - // 'sessionScreen' will handle screen sharing operations - sessionCamera = OVCamera.initSession(); - sessionScreen = OVScreen.initSession(); - - // --- 3) Specify the actions when events of type 'streamCreated' take - // --- place in the session. The reason why we're using two different objects - // --- is to handle diferently the subscribers when it is of 'CAMERA' type, or 'SCREEN' type --- - - // ------- 3.1) Handle subscribers of 'CAMERA' type - sessionCamera.on('streamCreated', event => { - if (event.stream.typeOfVideo == "CAMERA") { - // Subscribe to the Stream to receive it. HTML video will be appended to element with 'container-cameras' id - var subscriber = sessionCamera.subscribe(event.stream, 'container-cameras'); - // When the HTML video has been appended to DOM... - subscriber.on('videoElementCreated', event => { - // Add a new
element for the user's nickname just below its video - appendUserData(event.element, subscriber.stream.connection); - }); + // 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' || track.kind === 'screen') { + appendUserData(element, participant.identity); + } } - }); + ); - // ------- 3.2) Handle subscribers of 'Screen' type - sessionScreen.on('streamCreated', event => { - if (event.stream.typeOfVideo == "SCREEN") { - // Subscribe to the Stream to receive it. HTML video will be appended to element with 'container-screens' id - var subscriberScreen = sessionScreen.subscribe(event.stream, 'container-screens'); - // When the HTML video has been appended to DOM... - subscriberScreen.on('videoElementCreated', event => { - // Add a new
element for the user's nickname just below its video - appendUserData(event.element, subscriberScreen.stream.connection); - }); + // On every new Track destroyed... + room.on( + LivekitClient.RoomEvent.TrackUnsubscribed, + (track, publication, participant) => { + track.detach(); + document.getElementById(track.sid)?.remove(); + if (track.kind === 'video' || track.kind === 'screen') { + removeUserData(participant); + } } - }); + ); - // On every Stream destroyed... - sessionCamera.on('streamDestroyed', event => { - // Delete the HTML element with the user's nickname. HTML videos are automatically removed from DOM - removeUserData(event.stream.connection); - }); + // --- 3) Connect to the room with a valid access token --- - // On every asynchronous exception... - sessionCamera.on('exception', (exception) => { - console.warn(exception); - }); + // Get a token from the application backend + getToken(myRoomName, myUserName).then((token) => { + const livekitUrl = getLivekitUrlFromMetadata(token); - - // --- 4) Connect to the session with two different tokens: one for the camera and other for the screen --- - - // --- 4.1) Get the token for the 'sessionCamera' object - getToken(mySessionId).then(token => { - - // First param is the token got from the OpenVidu deployment. Second param can be retrieved by every user on event - // 'streamCreated' (property Stream.connection.data), and will be appended to DOM as the user's nickname - sessionCamera.connect(token, { clientData: myUserName }) + // First param is the LiveKit server URL. Second param is the access token + room + .connect(livekitUrl, token) .then(() => { + // --- 4) Set page layout for active call --- - // --- 5) Set page layout for active call --- - - document.getElementById('session-title').innerText = mySessionId; + document.getElementById('room-title').innerText = myRoomName; document.getElementById('join').style.display = 'none'; - document.getElementById('session').style.display = 'block'; + document.getElementById('room').style.display = 'block'; - // --- 6) Get your own camera stream with the desired properties --- + // --- 5) Publish your local tracks --- - var publisher = OVCamera.initPublisher('container-cameras', { - 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 'container-cameras' - mirror: false // Whether to mirror your local video or not + 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'; }); - - // --- 7) Specify the actions when events take place in our publisher --- - - // When our HTML video has been added to DOM... - publisher.on('videoElementCreated', function (event) { - initMainVideo(event.element, myUserName); - appendUserData(event.element, myUserName); - event.element['muted'] = true; - }); - - // --- 8) Publish your stream --- - sessionCamera.publish(publisher); - }) - .catch(error => { - console.log('There was an error connecting to the session:', error.code, error.message); + .catch((error) => { + console.log( + 'There was an error connecting to the room:', + error.code, + error.message + ); }); }); - - // --- 4.2) Get the token for the 'sessionScreen' object - getToken(mySessionId).then((tokenScreen) => { - // Create a token for screen share - sessionScreen.connect(tokenScreen, { clientData: myUserName }).then(() => { - document.getElementById('buttonScreenShare').style.visibility = 'visible'; - console.log("Session screen connected"); - }).catch((error => { - console.warn('There was an error connecting to the session for screen share:', error.code, error.message); - }));; - }); } -// --- 9) Function to be called when the 'Screen share' button is clicked -function publishScreenShare() { - // --- 9.1) To create a publisherScreen set the property 'videoSource' to 'screen' - var publisherScreen = OVScreen.initPublisher("container-screens", { videoSource: "screen" }); +function leaveRoom() { + // --- 6) Leave the room by calling 'disconnect' method over the Room object --- - // --- 9.2) Publish the screen share stream only after the user grants permission to the browser - publisherScreen.once('accessAllowed', (event) => { - document.getElementById('buttonScreenShare').style.visibility = 'hidden'; - screensharing = true; - // If the user closes the shared window or stops sharing it, unpublish the stream - publisherScreen.stream.getMediaStream().getVideoTracks()[0].addEventListener('ended', () => { - console.log('User pressed the "Stop sharing" button'); - sessionScreen.unpublish(publisherScreen); - document.getElementById('buttonScreenShare').style.visibility = 'visible'; - screensharing = false; - }); - sessionScreen.publish(publisherScreen); - }); - - publisherScreen.on('videoElementCreated', function (event) { - appendUserData(event.element, sessionScreen.connection); - event.element['muted'] = true; - }); - - publisherScreen.once('accessDenied', (event) => { - console.error('Screen Share: Access Denied'); - }); -} - -function leaveSession() { - - // --- 10) Leave the session by calling 'disconnect' method over the Session object --- - sessionScreen.disconnect(); - sessionCamera.disconnect(); + room.disconnect(); // Removing all HTML elements with user's nicknames. - // HTML videos are automatically removed when leaving a Session + // HTML videos are automatically removed when leaving a Room removeAllUserData(); - // Back to 'Join session' page + // Back to 'Join room' page document.getElementById('join').style.display = 'block'; - document.getElementById('session').style.display = 'none'; - // Restore default screensharing value to false - screensharing = false; + document.getElementById('room').style.display = 'none'; } +// + +async function toggleScreenShare() { + console.log('Toggling screen share'); + const enabled = !isScreenShared; + + if (enabled) { + // Enable screen sharing + screenSharePublication = await room.localParticipant?.setScreenShareEnabled(enabled); + + if (screenSharePublication) { + console.log('Screen sharing enabled', screenSharePublication); + isScreenShared = enabled; + + // Attach the screen share track to the video container + const element = screenSharePublication.track.attach(); + element.id = screenSharePublication.trackSid; + element.className = 'removable'; + document.getElementById('video-container').appendChild(element); + + // Add user data for the screen share + appendUserData(element, `${myUserName}_SCREEN`); + + // Listen for the 'ended' event to handle screen sharing stop + screenSharePublication.addListener('ended', async () => { + console.debug('Clicked native stop button. Stopping screen sharing'); + await stopScreenSharing(); + }); + } + } else { + // Disable screen sharing + await stopScreenSharing(); + } + } + + +async function stopScreenSharing() { + isScreenShared = false; + await room.localParticipant?.setScreenShareEnabled(false); + const trackSid = screenSharePublication?.trackSid; + + if (trackSid) { + document.getElementById(trackSid)?.remove(); + removeUserData({ identity: `${myUserName}_SCREEN` }); + } + screenSharePublication = undefined; + } + window.onbeforeunload = function () { - if (sessionCamera) sessionCamera.disconnect(); - if (sessionScreen) sessionScreen.disconnect(); + if (room) room.disconnect(); }; /* APPLICATION SPECIFIC METHODS */ @@ -190,39 +153,29 @@ window.addEventListener('load', function () { }); function generateParticipantInfo() { - document.getElementById("sessionId").value = "SessionScreenA"; - document.getElementById("userName").value = "Participant" + Math.floor(Math.random() * 100); + document.getElementById('roomName').value = 'RoomA'; + document.getElementById('userName').value = + 'Participant' + Math.floor(Math.random() * 100); } -function appendUserData(videoElement, connection) { - var userData; - var nodeId; - if (typeof connection === "string") { - userData = connection; - nodeId = connection; - } else { - userData = JSON.parse(connection.data).clientData; - nodeId = connection.connectionId; - } +function appendUserData(videoElement, participantIdentity) { var dataNode = document.createElement('div'); - dataNode.className = "data-node"; - dataNode.id = "data-" + nodeId; - dataNode.innerHTML = "
" + userData + "
"; + dataNode.className = 'removable'; + dataNode.id = 'data-' + participantIdentity; + dataNode.innerHTML = '' + participantIdentity + '
'; videoElement.parentNode.insertBefore(dataNode, videoElement.nextSibling); - addClickListener(videoElement, userData); + addClickListener(videoElement, participantIdentity); } -function removeUserData(connection) { - var dataNodeToRemove = document.getElementById("data-" + connection.connectionId); - if (dataNodeToRemove) { - dataNodeToRemove.parentNode.removeChild(dataNodeToRemove); - } +function removeUserData(participant) { + var dataNode = document.getElementById('data-' + participant.identity); + dataNode?.parentNode.removeChild(dataNode); } function removeAllUserData() { - var nicknameElements = document.getElementsByClassName('data-node'); - while (nicknameElements[0]) { - nicknameElements[0].parentNode.removeChild(nicknameElements[0]); + var elementsToRemove = document.getElementsByClassName('removable'); + while (elementsToRemove[0]) { + elementsToRemove[0].parentNode.removeChild(elementsToRemove[0]); } } @@ -230,66 +183,75 @@ 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').fadeOut('fast', () => { $('#main-video p').html(userData); mainVideo.srcObject = videoElement.srcObject; - $('#main-video').fadeIn("fast"); + $('#main-video').fadeIn('fast'); }); } }); } function initMainVideo(videoElement, userData) { - document.querySelector('#main-video video').srcObject = videoElement.srcObject; + document.querySelector('#main-video video').srcObject = + videoElement.srcObject; document.querySelector('#main-video p').innerHTML = userData; document.querySelector('#main-video video')['muted'] = true; } +function getLivekitUrlFromMetadata(token) { + if (!token) throw new Error('Trying to get metadata from an empty token'); + try { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + window + .atob(base64) + .split('') + .map((c) => { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join('') + ); + + const payload = JSON.parse(jsonPayload); + if (!payload?.metadata) throw new Error('Token does not contain metadata'); + const metadata = JSON.parse(payload.metadata); + return metadata.livekitUrl; + } catch (error) { + throw new Error('Error decoding and parsing token: ' + error); + } +} /** * -------------------------------------------- * GETTING A TOKEN FROM YOUR APPLICATION SERVER * -------------------------------------------- - * The methods below request the creation of a Session and a Token to + * 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. + * */ - var APPLICATION_SERVER_URL = "http://localhost:5000/"; +var APPLICATION_SERVER_URL = 'http://localhost:5000/'; - function getToken(mySessionId) { - return createSession(mySessionId).then(sessionId => createToken(sessionId)); - } - - function createSession(sessionId) { - return new Promise((resolve, reject) => { - $.ajax({ - type: "POST", - url: APPLICATION_SERVER_URL + "api/sessions", - data: JSON.stringify({ customSessionId: sessionId }), - headers: { "Content-Type": "application/json" }, - success: response => resolve(response), // The sessionId - error: (error) => reject(error) - }); - }); - } - - function createToken(sessionId) { - return new Promise((resolve, reject) => { - $.ajax({ - type: 'POST', - url: APPLICATION_SERVER_URL + 'api/sessions/' + sessionId + '/connections', - data: JSON.stringify({}), - headers: { "Content-Type": "application/json" }, - success: (response) => resolve(response), // The token - error: (error) => reject(error) - }); - }); - } \ No newline at end of file +function getToken(roomName, participantName) { + return new Promise((resolve, reject) => { + $.ajax({ + type: 'POST', + url: APPLICATION_SERVER_URL + 'token', + data: JSON.stringify({ + roomName, + participantName, + permissions: {}, + }), + headers: { 'Content-Type': 'application/json' }, + success: (token) => resolve(token), + error: (error) => reject(error), + }); + }); +} diff --git a/openvidu-js-screen-share/web/index.html b/openvidu-js-screen-share/web/index.html index 81ade8ac..49a75717 100644 --- a/openvidu-js-screen-share/web/index.html +++ b/openvidu-js-screen-share/web/index.html @@ -14,7 +14,7 @@ - + @@ -34,15 +34,15 @@