DEV Community

Cover image for Create a Video Party App With Rails Part 2: Building the Frontend
Ben Greenberg for Vonage

Posted on • Originally published at nexmo.com

Create a Video Party App With Rails Part 2: Building the Frontend

This is the second part of a two-part series on creating a video watch party application using the Vonage Video API and Ruby on Rails.

In the first article, we went through the steps of building the backend of the app. If you have not read that post yet, it would be a good place to start. Now we are going to focus on the frontend of our application. While the backend was written mainly in Ruby, the frontend will be a lot of client-side JavaScript.

Once we are done, we will have a video watch party app that we can use to chat with our friends and watch videos together!

Landing Page

Let's get started!

tl;dr If you would like to skip ahead and get right to deploying it, you can find all the code for the app and a one-click Deploy to Heroku button at the GitHub repository Deploy to Heroku.

Table of Contents

What Will We Be Building

Before we start coding, it is a good idea to take a moment and discuss what we will be building.

If you recall from the first post, we had instantiated a Video API Session ID, and are actively creating tokens for each participant. That information is being passed to the frontend by newly created JavaScript variables in the ERB view files. Additionally, we are also passing data from our environment variables to the frontend. We will be using all that information in the code we will write to create the experience of the app.

Ruby on Rails has come a long way in integrating client-side JavaScript directly into the stack with the introduction of Webpack in Rails starting with version 5.1. JavaScript is incorporated through packs placed inside /app/javascript/packs and added as either import or require() statements inside the application.js file inside the directory.

We will be separating out the various concerns of our code into different files so that at the end your folder will have the following files:

# app/javascript/packs

- application.js
- app_helpers.js
- chat.js
- opentok_screenshare.js
- opentok_video.js
- party.js
- screenshare.js
Enter fullscreen mode Exit fullscreen mode

Each file, besides application.js, will contain code to cover distinct concerns:

  • app_helpers.js: Cross-functional code that is needed across the frontend
  • chat.js: Creating a Chat class that will be used to instantiate instances of the text chat
  • opentok_screenshare.js: The client-side code for the Screenshare view
  • opentok_video.js: The client-side code for the Video Chat view
  • party.js: Creating a Party class that will be used to instantiate instances of the video chat
  • screenshare.js: Creating a Screenshare class that will be used to instantiate instances of the screenshare functionality

Prior to creating the code, let's add these files to the application.js file, which will instruct Webpack to compile them at runtime:

// application.js

import './app_helpers.js'
import './opentok_video.js'
import './opentok_screenshare.js'
Enter fullscreen mode Exit fullscreen mode

Creating the JavaScript Packs

In each subsection, we will create the JavaScript files that we enumerated above.

The app_helpers.js File

The app_helpers.js file will contain generic helper functions that we will export to the rest of the code to use throughout the app. We will create screenshareMode(), setButtonDisplay(), formatChatMsg(), and streamLayout() functions.

The screenshareMode() function will take advantage of the Vonage Video API Signal API to send a message to the browsers of all the participants that will trigger a window.location change. The Signal API is the same API we will use for the text chat, which is its simplest use case. However, as we will see in this function, the Signal API provides an intuitive and powerful way to direct the flow of your application simultaneously for all the participants without needing to write lots of code:

export function screenshareMode(session, mode) {
  if (mode == 'on') {
    window.location = '/screenshare?name=' + name;
    session.signal({
      type: 'screenshare',
      data: 'on'
    });
  } else if (mode == 'off') {
    window.location = '/party?name=' + name;
    session.signal({
      type: 'screenshare',
      data: 'off'
    });    
  };
};
Enter fullscreen mode Exit fullscreen mode

The next function, setButtonDisplay() changes the style for the HTML element containing the "Watch Mode On/Off" button to either be block or none depending on whether the participant is the moderator or not. There are many other ways to do this, including more secure methods. However, in order to keep things simple for this app to watch videos amongst friends, we will keep the keep minimalist:

export function setButtonDisplay(element) {
  if (name == moderator_env_name) {
    element.style.display = "block";
  } else {
    element.style.display = "none";
  };
};
Enter fullscreen mode Exit fullscreen mode

The formatChatMsg() function takes in the text message the participant sent as an argument and formats it for presentation on the site. This function looks for any text bracketed by two colons and attempts to parse the text inside those colons as an emoji. It also appends the participant's name to each message so everyone knows who is talking.

In order to add the emojis, we need to install a node package called node-emoji and we can do that by adding const emoji = require('node-emoji); to the top of the file and running yarn add node-emoji in the command line. The function will utilize match() with a regular expression to search for strings of text bookmarked by two colons, and if it matches, it will invoke the emoji const we defined to turn that string into an emoji:

export function formatChatMsg(message) {
  var message_arr;
  message_arr = message.split(' ').map(function(word) {
    if (word.match(/(?:\:)\b(\w*)\b(?=\:)/g)) {
      return word = emoji.get(word);
    } else {
      return word;
    }
  })
  message = message_arr.join(' ');
  return `${name}: ${message}`
};
Enter fullscreen mode Exit fullscreen mode

The last function inside app_helpers.js we need to create is streamLayout() that takes in arguments of the HTML element and the count of participants. The function will add or remove CSS classes to the element depending on the number of participants in order to change the video chat presentation into a grid format:

export function streamLayout(element, count) {
  if (count >= 6) {
    element.classList.add("grid9");
  } else if (count == 5) {
    element.classList.remove("grid9");
    element.classList.add("grid4");
  } else if (count < 5) {
    element.classList.remove("grid4");
  }
};
Enter fullscreen mode Exit fullscreen mode

The chat.js File

The chat.js code is going to create the Chat class using a constructor(). This Chat class will be called and instantiated in both the video chat and screenshare views:

// chat.js

import { formatChatMsg } from './app_helpers.js';

export default class Chat {
  constructor(session) {
    this.session = session;
    this.form = document.querySelector('form');
    this.msgTxt = document.querySelector('#message');
    this.msgHistory = document.querySelector('#history');
    this.chatWindow = document.querySelector('.chat');
    this.showChatBtn = document.querySelector('#showChat');
    this.closeChatBtn = document.querySelector('#closeChat');
    this.setupEventListeners();
  }
Enter fullscreen mode Exit fullscreen mode

We have given several properties to Chat, mostly based on different elemnts in the DOM and the Video API session. The last one, this.setupEventListeners() is invoking a function that we need to now add to the file:

  setupEventListeners() {
    let self = this;
    this.form.addEventListener('submit', function(event) {
      event.preventDefault();

      self.session.signal({
        type: 'msg',
        data: formatChatMsg(self.msgTxt.value)
      }, function(error) {
        if (error) {
          console.log('Error sending signal:', error.name, error.message);
        } else {
          self.msgTxt.value = '';
        }
      });
    });

    this.session.on('signal:msg', function signalCallback(event) {
      var msg = document.createElement('p');
      msg.textContent = event.data;
      msg.className = event.from.connectionId === self.session.connection.connectionId ? 'mine' : 'theirs';
      self.msgHistory.appendChild(msg);
      msg.scrollIntoView();
    });

    this.showChatBtn.addEventListener('click', function(event) {
      self.chatWindow.classList.add('active');
    });

    this.closeChatBtn.addEventListener('click', function(event) {
      self.chatWindow.classList.remove('active');
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

setupEventListeners() creates an EventListener for the text chat submit button. When a new message is submitted it is sent to the Signal API to be processed and sent to all the participants. Similarly, when a new message is received a new <p> tag is added to the chat element, and the participant's text chat window is scrolled to view it.

The next two files we will create perform similar functionality in creating new classes for the video chat party and for the screenshare view.

The party.js File

In this file we will create the Party class that will be used to instantiate new instances of the video chat:

// party.js

import { screenshareMode, setButtonDisplay, streamLayout } from './app_helpers.js';

export default class Party {
  constructor(session) {
    this.session = session;
    this.watchLink = document.getElementById("watch-mode");
    this.subscribers = document.getElementById("subscribers");
    this.participantCount = document.getElementById("participant-count");
    this.videoPublisher = this.setupVideoPublisher();
    this.clickStatus = 'off';
    this.setupEventHandlers();
    this.connectionCount = 0;
    setButtonDisplay(this.watchLink);
  }
Enter fullscreen mode Exit fullscreen mode

The constructor() function is given the Video API session as an argument and passes that to this.session. The rest of the properties are defined and given values. The watchLink, subscribers, participantCount properties come from the HTML elements, while videoPublisher is provided a function as its value, and clickStatus is given default of off.

We will create the setupVideoPublisher() function at this point. The function invokes the Video API JavaScript SDK initPublisher() function to start the video publishing. It can take in optional arguments, and as such, we specify that the video should occupy 100% of the width and height of its element and should be appended to the element:

  setupVideoPublisher() {
    return OT.initPublisher('publisher', {
      insertMode: 'append',
      width: "100%",
      height: "100%"
    }, function(error) {
      if (error) {
        console.error('Failed to initialise publisher', error);
      };
    });
  }
Enter fullscreen mode Exit fullscreen mode

There are several actions we also must create event listeners for and add them to the class. We need to listen for when the session is connected, when a video stream has been created, when a connction has been added and when a connection has been destroyed. When a connection has been added or destroyed, we either increment or decrement the participant count, and share the number of participants in the participant count <div> element on the page:

  setupEventHandlers() {
    let self = this;
    this.session.on({
      // This function runs when session.connect() asynchronously completes
      sessionConnected: function(event) {
        // Publish the publisher we initialzed earlier (this will trigger 'streamCreated' on other
        // clients)
        self.session.publish(self.videoPublisher, function(error) {
          if (error) {
            console.error('Failed to publish', error);
          }
        });
      },

      // This function runs when another client publishes a stream (eg. session.publish())
      streamCreated: function(event) {
        // Subscribe to the stream that caused this event, and place it into the element with id="subscribers"
        self.session.subscribe(event.stream, 'subscribers', {
          insertMode: 'append',
          width: "100%",
          height: "100%"
        }, function(error) {
          if (error) {
            console.error('Failed to subscribe', error);
          }
        });
      },

      // This function runs whenever a client connects to a session
      connectionCreated: function(event) {
        self.connectionCount++;
        self.participantCount.textContent = `${self.connectionCount} Participants`;
        streamLayout(self.subscribers, self.connectionCount);
      },

      // This function runs whenever a client disconnects from the session
      connectionDestroyed: function(event) {
        self.connectionCount--;
        self.participantCount.textContent = `${self.connectionCount} Participants`;
        streamLayout(self.subscribers, self.connectionCount);
      }
    });
Enter fullscreen mode Exit fullscreen mode

Lastly, we add one more event listener. This event listener is attached to the click action on the "Watch Mode On/Off" button. When it is clicked it goes to the screenshare view, if the click status was off. You will recall that the click status is given a default of off in the construction of the class:

    this.watchLink.addEventListener('click', function(event) {
      event.preventDefault();
      if (self.clickStatus == 'off') {
        // Go to screenshare view
        screenshareMode(self.session, 'on');
      };
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The screenshare.js File

The final class we will create is a Screenshare class that will be responsible for defining the video screenshare. The constructor() function takes the Video API session and the participant's name as arguments:

// screenshare.js

import { screenshareMode } from './app_helpers.js';

export default class Screenshare {
  constructor(session, name) {
    this.session = session;
    this.name = name;
    this.watchLink = document.getElementById("watch-mode");
    this.clickStatus = 'on';
  }
Enter fullscreen mode Exit fullscreen mode

Unlike the Party class, the clickStatus here defaults to on since we want to move away from the screenshare and back to the video chat mode, if the moderator clicks the "Watch Mode On/Off" button.

We also utilize toggle() to either share the participant's screen, if the participant is the moderator, or subscribe to the screenshare for everyone else:

  toggle() {
    if (this.name === moderator_env_name) {
      this.shareScreen();
    } else {
      this.subscribe();
    }
  }
Enter fullscreen mode Exit fullscreen mode

The shareScreen() function invoked in the toggle() needs to be defined:

  shareScreen() {
    this.setupPublisher();
    this.setupAudioPublisher();
    this.setupClickStatus();
  }
Enter fullscreen mode Exit fullscreen mode

This function itself has three functions that need to also be created. The first function will publish the screen of the moderator. However, the screen publishing by itself does not also include audio. Therefore, a second function will publish the audio from the moderator's computer. Then, the final function in shareScreen() will move back to the video chat view if the "Watch Mode On/Off" button is clicked:

setupClickStatus() {
    // screen share mode off if clicked off
    // Set click status
    let self = this;
    this.watchLink.addEventListener('click', function(event) {
      event.preventDefault();
      if (self.clickStatus == 'on') {
        self.clickStatus = 'off';
        screenshareMode(self.session, 'off');
      };
    });
  }

  setupAudioPublisher() {
    var self = this;
    var audioPublishOptions = {};
    audioPublishOptions.insertMode = 'append';
    audioPublishOptions.publishVideo = false;
    var audio_publisher = OT.initPublisher('audio', audioPublishOptions,
      function(error) {
        if (error) {
          console.log(error);
        } else {
          self.session.publish(audio_publisher, function(error) {
            if (error) {
              console.log(error);
            }
          });
        };
      }
    );
  }

  setupPublisher() {
    var self = this;
    var publishOptions = {};
    publishOptions.videoSource = 'screen';
    publishOptions.insertMode = 'append';
    publishOptions.height = '100%';
    publishOptions.width = '100%';
    var screen_publisher = OT.initPublisher('screenshare', publishOptions,
      function(error) {
        if (error) {
          console.log(error);
        } else {
          self.session.publish(screen_publisher, function(error) {
            if (error) {
              console.log(error);
            };
          });
        };
      }
    );
  }
Enter fullscreen mode Exit fullscreen mode

All the above is in order to create the screenshare for the moderator. Everyone else in the app will want to subscribe to that screenshare. We will use the subscribe() function to do that. This will be the last function inside the file:

  subscribe() {
    var self = this;
    this.watchLink.style.display = "none";
    this.session.on({
      streamCreated: function(event) {
        console.log(event);
        if (event.stream.hasVideo == true) {
          self.session.subscribe(event.stream, 'screenshare', {
            insertMode: 'append',
            width: '100%',
            height: '100%'
          }, function(error) {
            if (error) {
              console.error('Failed to subscribe to video feed', error);
            }
          });
        } else if (event.stream.hasVideo == false ) {
          self.session.subscribe(event.stream, 'audio', {
            insertMode: 'append',
            width: '0px',
            height: '0px'
          }, function(error) {
            if (error) {
              console.error('Failed to subscribe to audio feed', error);
            }
          });
        };
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

We are now ready to make all these classes we have defined work in the application by creating instances of them inside the opentok_screenshare.js and opentok_video.js files.

Creating opentok_video.js

The opentok_video.js file will build a new video chat experience. Most of the work was done in the classes we defined above, so this file is relatively small. First, let's import the Chat and Party classes:

// opentok_video.js

import Chat from './chat.js'
import Party from './party.js'
Enter fullscreen mode Exit fullscreen mode

Then, we will define a global empty variable to hold the Video API session:

var session = ''
Enter fullscreen mode Exit fullscreen mode

Then we wrap the rest of the code in three checks to make sure we are on the correct website path, that the DOM is fully loaded and that the participant name is not empty:

if (window.location.pathname == '/party') {
  document.addEventListener('DOMContentLoaded', function() {
    if (name != '') {
Enter fullscreen mode Exit fullscreen mode

The rest of the code initiates a new Video API session if one does not exist and instantiates a new Chat and new Party. At the end, we also listen for the Signal API to send a screenshare data message with the value of on. When that message is received the window.location is moved to /screenshare:

      // Initialize an OpenTok Session object
      if (session == '') {
        session = OT.initSession(api_key, session_id);
      }

      new Chat(session);
      new Party(session);

      // Connect to the Session using a 'token'
      session.connect(token, function(error) {
        if (error) {
          console.error('Failed to connect', error);
        }
      });

      // Listen for Signal screenshare message
      session.on('signal:screenshare', function screenshareCallback(event) {
        if (event.data == 'on') {
          window.location = '/screenshare?name=' + name;
        };
      });
    };
  });
}
Enter fullscreen mode Exit fullscreen mode

Creating opentok_screenshare.js

The last JavaScript file we will create is mightily similar to the last one. It is responsible for the screenshare view and leverages the Screenshare and Chat classes we defined earlier:


import Screenshare from './screenshare.js'
import Chat from './chat.js'

// declare empty global session variable
var session = ''

if (window.location.pathname == '/screenshare') {
  document.addEventListener('DOMContentLoaded', function() {
    // Initialize an OpenTok Session object
    if (session == '') {
      session = OT.initSession(api_key, session_id);
    }

    // Hide or show watch party link based on participant
    if (name != '' && window.location.pathname == '/screenshare') {
      new Chat(session);
      new Screenshare(session, name).toggle();

      // Connect to the Session using a 'token'
      session.connect(token, function(error) {
        if (error) {
          console.error('Failed to connect', error);
        }
      });

      // Listen for Signal screenshare message
      session.on('signal:screenshare', function screenshareCallback(event) {
        if (event.data == 'off') {
          window.location = '/party?name=' + name;
        };
      });
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

Before we can wrap this up, last but certainly not least, we need to define the frontend style of the application. All this code is useless if it is not accessible by the participants.

Styling the Application

The stylesheet for this application would not have happened without the help of my friend and former colleague, Hui Jing Chen who taught me a lot about front-end design through this process. The app primarily uses Flexbox Grid to order the elements.

Let's start by creating a custom.css file inside app/javascript/stylesheets. We want to make sure that it is included in our application so add an import line to application.scss in the same folder, @import './custom.css';.

First, let's add the core styling in custom.css:

:root {
  --main: #343a40;
  --txt-alt: white;
  --txt: black;
  --background: white;
  --bgImage: url('~images/01.png');
  --chat-bg: rgba(255, 255, 255, 0.75);
  --chat-mine: darkgreen;
  --chat-theirs: indigo;
}

html {
  box-sizing: border-box;
  height: 100%;
}

*,
*::before,
*::after {
  box-sizing: inherit;
  margin: 0;
  padding: 0;
}

body {
  height: 100%;
  display: flex;
  flex-direction: column;
  background-color: var(--background);
  background-image: var(--bgImage);
  overflow: hidden;
}

main {
  flex: 1;
  display: flex;
  position: relative;
}

input {
  font-size: inherit;
  padding: 0.5em;
  border-radius: 4px;
  border: 1px solid currentColor;
}

button,
input[type="submit"] {
  font-size: inherit;
  padding: 0.5em;
  border: 0;
  background-color: var(--main);
  color: var(--txt-alt);
  border-radius: 4px;
}

header {
  background-color: var(--main);
  color: var(--txt-alt);
  padding: 0.5em;
  height: 4em;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
Enter fullscreen mode Exit fullscreen mode

Then, let's add the styling for the landing page:

.landing {
  margin: auto;
  text-align: center;
  font-size: 125%;
}

.landing form {
  display: flex;
  flex-direction: column;
  margin: auto;
  position: relative;
}

.landing input,
.landing p {
  margin-bottom: 1em;
}

.landing .error {
  color: maroon;
  position: absolute;
  bottom: -2em;
  width: 100%;
  text-align: center;
}
Enter fullscreen mode Exit fullscreen mode

We also want to add the styling for the text chat, especially making sure that it stays in place and does not scroll the whole page as it progresses:

.chat {
  width: 100%;
  display: flex;
  flex-direction: column;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 2;
  background-color: var(--chat-bg);
  transform: translateX(-100%);
  transition: transform 0.5s ease;
}

.chat.active {
  transform: translateX(0);
}

.chat-header {
  padding: 0.5em;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.24);
  display: flex;
  justify-content: space-between;
}

.btn-chat {
  height: 5em;
  width: 5em;
  border-radius: 50%;
  box-shadow: 0 3px 6px 0 rgba(0, 0, 0, .2), 0 3px 6px 0 rgba(0, 0, 0, .19);
  position: fixed;
  right: 1em;
  bottom: 1em;
  cursor: pointer;
}

.btn-chat svg {
  height: 4em;
  width: 2.5em;
}

.btn-close {
  height: 2em;
  width: 2em;
  background: transparent;
  border: none;
  cursor: pointer;
}

.btn-close svg {
  height: 1em;
  width: 1em;
}

.messages {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow-y: scroll;
  padding: 1em;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.24);
  scrollbar-color: #c1c1c1 transparent;
}

.messages p {
  margin-bottom: 0.5em;
}

.mine {
  color: var(--chat-mine);
}

.theirs {
  color: var(--chat-theirs);
}

.chat form {
  display: flex;
  padding: 1em;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.24);
}

.chat input[type="text"] {
  flex: 1;
  border-top-left-radius: 0px;
  border-bottom-left-radius: 0px;
  background-color: var(--background);
  color: var(--txt);
  min-width: 0;
}

.chat input[type="submit"] {
  border-top-right-radius: 0px;
  border-bottom-right-radius: 0px;
}
Enter fullscreen mode Exit fullscreen mode

Now let's create the styling for the video chat and screenshare elements:

.videos {
  flex: 1;
  display: flex;
  position: relative;
}

.subscriber.grid4 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(25em, 1fr));
}

.subscriber.grid9 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18em, 1fr));
}

.subscriber,
.screenshare {
  width: 100%;
  height: 100%;
  display: flex;
}

.publisher {
  position: absolute;
  width: 25vmin;
  height: 25vmin;
  min-width: 8em;
  min-height: 8em;
  align-self: flex-end;
  z-index: 1;
}

.audio {
  position: absolute;
  opacity: 0;
  z-index: -1;
}

.audio {
  display: none;
}

.dark {
  --background: black;
  --chat-mine: lime;
  --chat-theirs: violet;
  --txt: white;
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we will add a media query that will keep the text chat in proportion on smaller screens:

@media screen and (min-aspect-ratio: 1 / 1) {
  .chat {
    width: 20%;
    min-width: 16em;
  }
}
Enter fullscreen mode Exit fullscreen mode

That's it! The application, both the backend and the frontend, has been created. We are now ready to put it all together.

Putting It All Together

Even though the application is a combination of multiple programming languages, namely Ruby and JavaScript, with an intertwined backend and frontend, it is relatively straightforward to run it. This is because Rails allows us to seamlessly integrate it all together with one command.

From the command line, you can execute bundle exec rails s and watch your Rails server start. You will also see the following almost magical line in your console output the first time you run the app:

[Webpacker] Compiling...
Enter fullscreen mode Exit fullscreen mode

In fact, you will see that every time you make a change to any of your JavaScript or CSS packs. That output tells you that Rails is using Webpack to compile and incorporate all of your packs into the application. Once the [Webpacker] Compiling... is done you will see a list of all your compiled packs:

Version: webpack 4.42.1
Time: 1736ms
Built at: 05/01/2020 12:01:37 PM
                                             Asset      Size               Chunks                         Chunk Names
            js/app_helpers-31c49752d24631573287.js   100 KiB          app_helpers  [emitted] [immutable]  app_helpers
        js/app_helpers-31c49752d24631573287.js.map  44.3 KiB          app_helpers  [emitted] [dev]        app_helpers
            js/application-d253fe0e7db5e2b1ca60.js   564 KiB          application  [emitted] [immutable]  application
        js/application-d253fe0e7db5e2b1ca60.js.map   575 KiB          application  [emitted] [dev]        application
                   js/chat-451fca901a39ddfdf982.js   103 KiB                 chat  [emitted] [immutable]  chat
               js/chat-451fca901a39ddfdf982.js.map  46.1 KiB                 chat  [emitted] [dev]        chat
    js/opentok_screenshare-2bc51be74c7abf27abe2.js   110 KiB  opentok_screenshare  [emitted] [immutable]  opentok_screenshare
js/opentok_screenshare-2bc51be74c7abf27abe2.js.map    51 KiB  opentok_screenshare  [emitted] [dev]        opentok_screenshare
          js/opentok_video-15ed35dc7b01325831c0.js   109 KiB        opentok_video  [emitted] [immutable]  opentok_video
      js/opentok_video-15ed35dc7b01325831c0.js.map  50.6 KiB        opentok_video  [emitted] [dev]        opentok_video
                  js/party-f5d6c0ccd3bb1fcc225e.js   105 KiB                party  [emitted] [immutable]  party
              js/party-f5d6c0ccd3bb1fcc225e.js.map  47.5 KiB                party  [emitted] [dev]        party
            js/screenshare-4c13687e1032e93dc59a.js   105 KiB          screenshare  [emitted] [immutable]  screenshare
        js/screenshare-4c13687e1032e93dc59a.js.map  47.9 KiB          screenshare  [emitted] [dev]        screenshare
                                     manifest.json  2.38 KiB                       [emitted]              
Enter fullscreen mode Exit fullscreen mode

The file names reflect that they have been compiled down, but you can still see your pack names in there if you look closely, like opentok_screenshare, party, app_helpers, etc.

Running your application locally is great for testing with yourself, but you probably would like to invite friends to participate with you!

You can create an externally accessible link to your application running locally using a tool like ngrok. It gives an external URL for your local environment. The Nexmo Developer Platform has a guide on getting up and running with ngrok that you can follow.

If you would like to just get up and running, you can also deploy with one click this application from GitHub directly to Heroku. Click on the Deploy to Heroku button and within moments you will have a watch party app ready to be used and enjoyed.

I would love to hear what you built using the Vonage Video API! Please join the conversation on our Community Slack and share your story!

Top comments (13)

Collapse
 
leastbad profile image
leastbad

Great finish, Ben!

Just curious, as I'm still learning about webpack every day... how come you put each class into the packs folder, essentially creating n packs?

Usually I see application.js importing from '../controllers' or whatever is appropriate to whatever framework is in place. Your configuration seems to essentially treat each pack as a separate file that needs to be sent down the wire, even though you don't appear to be doing any dynamic imports.

I'm not so uptight about a batch of HTTP requests, but I am curious if you had some great intent behind it.

Final question for the moment: do you have to load the OpenTok script on your global window object via a SCRIPT tag, or is there an npm package we can import?

Collapse
 
bengreenberg profile image
Ben Greenberg

Hello again,

Thanks so much!

There are different ways people choose to organize their assets, whether the stylesheets, javascript or images. If this was going to be a production app that I could reasonably expect would grow in complexity and in usage, I would probably do it differently. However, as the use case here was something fun for kids (or any other group of friends, etc.) I was less concerned with adopting a specific opinion on Webpack asset folder structure. As an aside, if this was to be anything more than what it was intended for, I also would not recommend implementing the "security" protocols the way I did either, as that is not very secure! A real production app would want to have more than hiding the moderator functions with a CSS element style! :)

There is indeed a Node SDK for OpenTok. Reference docs are here: tokbox.com/developer/sdks/node/.

Collapse
 
leastbad profile image
leastbad

I actually meant on the client: github.com/nexmo-community/rails-v...

I'm a Rails dev. :)

Collapse
 
leastbad profile image
leastbad

So, yeah... now that I've clarified I meant "is there a way to import the OpenTok library without needing a script tag in the document"... is there? Not a rhetorical question. Hoping to put this API to work!

Thread Thread
 
bengreenberg profile image
Ben Greenberg

Yes, there is, look at the last sentence in my last reply :)

There is indeed a Node SDK for OpenTok. Reference docs are here: tokbox.com/developer/sdks/node/.

Thread Thread
 
leastbad profile image
leastbad • Edited

I am clearly failing to communicate what I'm trying to ask. Let me try harder/better.

  1. I'm a Rails developer, not a Node developer - so you can safely presume that I'm not looking for a Node library in any potential future inquiry. ;)

  2. What I am trying to do is load whatever client library is necessary via my webpack application.js pack file, specifically so that I don't have to resort to putting a script tag in my HEAD and addressing the TokBox classes via the global namespace.

I think what I'm looking for is an ES6 module, probably downloaded and added to my package.json via npm add?

FWIW, the answer could very well be "no, we haven't done that (yet)"!

Thread Thread
 
bengreenberg profile image
Ben Greenberg

Not a problem, I think I understand now.

If I understand correctly, then the answer is you can add the OpenTok JS/Node SDK to your application via Webpack with yarn add opentok or npm install opentok without the use of a script tag.

Thread Thread
 
leastbad profile image
leastbad

Thanks for trying, Ben. That package is, once again, the node server implementation. I'm looking for the client. :)

It's all good... I think that the @opentok/client is the module I'm looking for. I'll let you know how I make out!

Thread Thread
 
bengreenberg profile image
Ben Greenberg

Glad you followed the link! Best of luck to you!

Collapse
 
kamalpandey profile image
kamalpandey

Thanks for the tutorial. Is there any way we can share desktop and allow remote control of dashboard of each others.

Collapse
 
bengreenberg profile image
Ben Greenberg

Hi Kamal,

Unfortunately, it's not possible to remotely control the screenshare. The screenshare functions as a published video stream in the same way that a webcam is a published video stream, except the media source is the screen instead of the camera. Just like a camera is a one-way stream, (i.e. viewers view the camera), so too with the screenshare stream.

All the best,

Ben

Collapse
 
kamalpandey profile image
kamalpandey

Hi Ben,

Could you help me with this ?

I am trying to implement group video functionality and I have following code

class App extends Component {
  this.state = {
     subscribers: [],
     subscriberVideos: [],
     screenVideo: undefined,
     publisherVideo: undefined,
     publisher: undefined,
  }
}
async start() {
  let publisher = OT.initPublisher(null, this.defaultPublisherProps, error => {
    if(error) {
    }  
  })
 publisher.on('videoElementCreated', this.onPublisherVideoElement);
 this.state.otSession.publish(publisher, error => {
    if (error) {
   }
   this.setState(() => {
     return { publisher: publisher };
   }
  }
}

onPublisherVideoElement = event {
  this.setState(() => {
   return { publisherVideo: event.element }
   });
  }
}
onPublisherScreenElement = event => {
  this.setState(() => {
    return { screenVideo: event.element }
 }
}
onSubscriberVideoElement = event => {
   this.setState(() => {
      return { 
        subscriberVideos: [...this.state.subscriberVideos, event.element]
      }
     }
   })
}
onSessionStreamCreated = event => {
  let subscriber = this.state.otSession.subscriber(event.stream, null, this.defaultSubscriberProps, error => {
     if (error) {
     }
    if (event.stream.videoType == 'screen') {
      subscriber.on('videoElementCreated', this.onPublisherScreenElement);
      this.setState(() => {
        return { screenSubscriber: subscriber };
      })
   } else {
     subscriber.on('videoElementCreated', this.onSubscriberVideoElement);
       if (this.props.mdm === "m") {
          this.setState((prevState) => {
             return {
               subscribers: [...this.state.subscribers, subscriber]
             }
          }
        }
     else {
       return { subscribers: [...this.state.subscribers, subscriber] }
     }
   }
  }
}

While showing subscribers I am doing

showSubscribers() {
    return this.state.subscribers.filter(subscriber => subscriber.stream !== null).map((subscriber, index) => {
       return(
          <VideoSubscriber
             key={index}
             otok={this.otok}
             subscriber={this.state.subscribers[index]
             subscriberVidoe={this.state.subscriberVideos[index]
            />
       )
    }
}

But at random times the video goes black and also while some of the subscriber refresh the page it goes blank creates multiple subscirber with null stream.
How can i resolve this?

Thread Thread
 
bengreenberg profile image
Ben Greenberg

Hi Kamal,

Good to hear from you here and in the Video Ruby SDK where you raised an issue with this question and in our Community Slack! Instead of responding in 3 separate places to the same question, we're following up with you in the Community Slack.

If anyone has any questions about the Video API, or any other Vonage API, please feel free to join us in the Vonage Community Slack where we would be glad to lend a hand!