Solomon Eseme

Vonage Team Member

A Software Developer who is geared towards building high performing and innovative products following best practices and industry standards. He also loves writing about backend development.

[Read more]
< Tutorial />

Creating an Online Classroom in Laravel With Vonage Video API

Last updated on Dec 11, 2020

This article will demonstrate how to set up a real-time Video/Virtual Classroom using the Vonage Video API where the teacher can be in any location in the world and still communicate and have lectures with the student in real-time.

What Exactly is Vonage Video API?

Vonage Video API is a service that makes it easy for developers to create applications that utilize WebRTC and Web Sockets to give real-time communication by video, text, audio, and others for web apps, mobile apps, and even desktop devices. They provide powerful SDKs for various platforms to implement and give users the API to use for their applications.

How Does It Work?

All applications that utilize the Vonage Video API consist of three main components and everything you do revolves around them. They are:

  1. Sessions
  2. Server
  3. Client

Let’s look at each of them in a little bit of detail:

Sessions

A session can be seen as the virtual chatroom where communications take place. In order for two computers to be able to exchange video or audio, they have to be connected to the same session. Sessions are hosted/stored in Vonage API’s cloud and they all have a unique ID. Sessions manage the user streams (video and audio feed) and keep track of all events (such as when users join or leave or send a text message).

Server

Servers are the backend code you write to manage the sessions in your application. For communication to take place, your server has to communicate with Vonage API’s cloud to create a session and use the session ID received to generate tokens that clients (browsers and mobile phones) can then use to join the session and exchange streams. In order to make your server work, you’d have to use one of Vonage API’s many server side SDKs available for various server-side languages.

Client

Clients are the browsers or mobile devices that users interact with directly to exchange video and audio feeds. Clients always require a token (generated by your server) to join a session so they can interact with other clients on the same session.

When a client is connected, they are able to publish (send video/audio feed) and subscribe (receive audio/video feed) to other clients on the same session. A client could either be a publisher, subscriber, or moderator.

Publishers are allowed to send and receive audio/video feed. Subscribers aren’t allowed to send, but they receive audio/video feed. Moderators are allowed to do what publishers can, but also prevent other clients from subscribing.

To recap, this is what happens for communication to occur when using the Vonage API: 

Your server creates a session on the Vonage API Cloud that has a unique ID. When a client wants to join, your server uses the session ID to generate a token for your client. The client joins the session and publishes streams to the session. When another client joins, the server generates a new token, and the two clients can subscribe to each other in the session and receive each other’s streams.

Setting Up Our Online Class with Vonage API

In this tutorial, we’ll be setting up a basic video feed with the Vonage API to allow clients to send audio and video feed. Vonage API provides an SDK for PHP and we’ll be using it to create our virtual class.

Set Up a Laravel Project

The first step in creating our virtual class is setting up a Laravel Project.

It’s important to note that at the time of writing this, the Vonage API doesn’t work with the latest version of Laravel* *(8.x). This is because the Vonage API PHP SDK depends on version 6 of GuzzleHTTP but Laravel 8 uses version 7 of GuzzleHTTP. So for this tutorial, please install version 7 of Laravel. You can do so by running the install command like this:

composer create-project --prefer-dist laravel/laravel:^7.0 virtual_classroom

Or you can also follow along by cloning the repository here.

Installing Vonage API PHP SDK

The next step is installing the Vonage API PHP SDK. We do that by installing the package with composer to our project.

composer require opentok/opentok 4.4.x -W

Defining our Migrations and Models

Now we have to define our teachers and students so they can have different permissions in our virtual classroom. Our application would make use of two tables — users to store all the teachers & students and virtual_classes to store the session ids of our classes so that students can see the ongoing class when they log in and join it. These are the migrations for both tables:

Open the users migration at database/migrations/xxxxxx_create_users_xxxx.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */

    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();

            // This enum field tells us if the current user is a teacher or student

            $table->enum('user_type', ['Student', 'Teacher']);
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */

    public function down()
    {
        Schema::dropIfExists('users');
    }
}

Open the virtual_classes migration at database/migrations/xxxxxx_create_virtual_classes_xxxx.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateVirtualClassesTable extends Migration
{
   /**
     * Run the migrations.
     *
     * @return void
     */

    public function up()
    {
        Schema::create('virtual_classes', function (Blueprint $table) {
            $table->id();
            $table->string("name");

            // User id to know which teacher created the class

            $table->integer("user_id");            `
            $table->string("session_id");
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */

    public function down()
    {
        Schema::dropIfExists('classes');
    }
}

After doing this, we’ll need to create a folder called Models in the app directory and move our models there. Then, we’ll update the namespace to App\Models on our models. Finally, we’ll update the User model to add user_type to the $fillable array and create a relationship to virtual classes so we can access them easily.

Open the User model at app/Models/Users.php


//REMEMBER TO UPDATE THE NAMESPACE AFTER MOVING THE FILE

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */

    // DON'T FORGET TO ADD THE USER_TYPE TO THIS ARRAY

    protected $fillable = [
        'name', 'email', 'password', 'user_type'
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */

    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    // Relationship tying a virtual class to a user (teacher in our case)

    public function myClass() {
        return $this->hasOne(VirtualClass::class);
    }
}

Authentication

Now that we have our model architecture setup, we need to allow users to register and log in. The first thing to do is to edit the providers in our project configuration. Since we changed the location of the User model, we need to reflect the change. We first go to config/auth.php and edit the providers array and run the commands to scaffold the authentication.

```php    'providers' => [         'users' => [             'driver' => 'eloquent',             'model' => App\Models\User::class, // New location of User model         ],

        // 'users' => [         //     'driver' => 'database',         //     'table' => 'users',         // ],     ],

// Rest of auth.php file ```

After doing this, we simply run the Laravel default authentication scaffold:

composer require laravel/ui:^2.4
php artisan ui vue --auth
npm install
npm run dev

After running all four commands in that order, Laravel should add the required views to setup authentication. The final step in setting up our authentication is to edit the RegisterController and the register.blade.php file to know if a user is a student or teacher at the time of registration. 

First, the app/Http/Controllers/Auth/RegisterController file:

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;

// PLEASE REMEMBER TO CHANGE THE IMPORT STATEMENT HERE

use App\Models\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Register Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users as well as their
    | validation and creation. By default, this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
    */

    use RegistersUsers;

    /**
     * Where to redirect users after registration.
     *
     * @var string
     */

    protected $redirectTo = RouteServiceProvider::HOME;

    /**
     * Create a new controller instance.
     *
     * @return void
     */

    public function __construct()
    {
        $this->middleware('guest');
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param array $data
     * @return \Illuminate\Contracts\Validation\Validator
     */

    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],

            // Add the user_type here for validation.
            'user_type' => ['required', 'string', 'in:Student,Teacher'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param array $data
     * @return \App\User
     */

    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'user_type' => $data['user_type'],
            'password' => Hash::make($data['password']),
        ]);
    }
}

Then the register view. It’s found in resources/views/auth/register.blade.php:

@extends('layouts.app')
@section('content')

<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Register') }}</div>


                <div class="card-body">
                   <form method="POST" action="{{ route('register') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>


                            <div class="col-md-6">
                                <input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name') }}" required autocomplete="name" autofocus>

                                @error('name')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email">

                                @error('email')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="user_type" class="col-md-4 col-form-label text-md-right">{{ __('User Type') }}</label>

{{--                        Note the select box here???--}}

                            <div class="col-md-6">
                                <select id="user_type" type="text" class="form-control @error('user_type') is-invalid @enderror" name="user_type" required>
                                    <option value="Student">Student</option>
                                    <option value="Teacher">Teacher</option>
                                </select>

                                @error('user_type')

                                <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">

                                @error('password')
                                   <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>

                            <div class="col-md-6">
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Register') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

@endsection

With this done, we should be able to register and login as either a teacher or a student.

Setting up Our Server

Before you can set up your server, you'd need a project API key and secret. To get that, you'd have to create a free Vonage API Account. Please do that by clicking this link. Once you follow through and create a project, then feel free to come back and continue :)

First, let’s store our API key and API secret in our .env file in our Laravel project:

### ADD THESE LINES AT THE BOTTOM OF YOUR .env FILE, OR WHEREVER REALLY ###

VONAGE_API_KEY=your_api_key
VONAGE_API_SECRET=your_api_secret

### REST OF .ENV FILE ###

Now, we'll need to set up a controller actually to set up sessions and generate tokens for new users. Let’s create one and call it SessionsController:

php artisan make:controller SessionsController

Let’s fill it with the necessary methods in app/Http/Controllers/SessionsController.php:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\VirtualClass;

#Import necessary classes from the Vonage API (AKA OpenTok)

use OpenTok\OpenTok;
use OpenTok\MediaMode;
use OpenTok\Role;

class SessionsController extends Controller
{
    /** Creates a new virtual class for teachers
     *
     * @param Request $request
     * @return \Illuminate\Http\RedirectResponse
     */

    public function createClass(Request $request)
    {
        // Get the currently signed-in user
        $user = $request->user();
        // Throw 403 if student tries to create a class
        if ($user->user_type === "Student") return back(403);
        // Instantiate a new OpenTok object with our api key & secret
        $opentok = new OpenTok(env('VONAGE_API_KEY'), env('VONAGE_API_SECRET'));
        // Creates a new session (Stored in the Vonage API cloud)
        $session = $opentok->createSession(array('mediaMode' => MediaMode::ROUTED));
        // Create a new virtual class that would be stored in db
        $class = new VirtualClass();
        // Generate a name based on the name the teacher entered
        $class->name = $user->name . "'s " . $request->input("name") . " class";
        // Store the unique ID of the session
        $class->session_id = $session->getSessionId();
        // Save this class as a relationship to the teacher
        $user->myClass()->save($class);
        // Send the teacher to the classroom where real-time video goes on
        return redirect()->route('classroom', ['id' => $class->id]);
    }

    public function showClassRoom(Request $request, $id)
    {
        // Get the currently authenticated user
        $user = $request->user();
        // Find the virtual class associated by provided id
        $virtualClass = VirtualClass::findOrFail($id);
        // Gets the session ID
        $sessionId = $virtualClass->session_id;
        // Instantiates new OpenTok object
        $opentok = new OpenTok(env('VONAGE_API_KEY'), env('VONAGE_API_SECRET'));
        // Generates token for client as a publisher that lasts for one week
        $token = $opentok->generateToken($sessionId, ['role' => Role::PUBLISHER, 'expireTime' => time() + (7 * 24 * 60 * 60)]);
        // Open the classroom with all needed info for clients to connect
        return view('classroom', compact('token', 'user', 'sessionId'));
    }
}

When creating the token, we can set the role the current client can have. It could be either Publisher, Subscriber or Moderator. The roles are explained above. In the code snippet above, we’re giving all users publisher status, meaning all users can send and receive streams(video-audio feed)

We’ll need to create a classroom.blade.php file in our resources/views folder for the showClassroom method. 

We’ll also need to create routes for the controller. We’ll add these routes in the routes/web.php file:

<?php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

// Add this to your web.php file

// This line makes all routes in it to use the auth middleware, meaning only signed-in users can access these routes

Route::middleware('auth')->group(function () {

    // This route creates classes for teachers

    Route::post("/create_class", 'SessionsController@createClass')
        ->name('create_class');

    // This route is used by both teachers and students to join a class

    Route::get("/classroom/{id}", 'SessionsController@showClassRoom')
        ->where('id', '[0-9]+')
        ->name('classroom');
});

Finally, in our dashboard, we need to give teachers a point to create classes and give students the list of classes to join. To do this, we'll need to update the HomeController and the home.blade.php file. Let's do this; I hope y'all are still with me 🙂 

We’ll start with the app/Http/Controllers/HomeController:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\ClassModel as VirtualClass;

class HomeController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */

    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */

    public function index(Request $request)
    {
        $user = $request->user();
        $classes = [];

        // If user is a student, give her a list of virtual classes

        if ($user->user_type === "Student") {
            $classes = VirtualClass::orderBy('name', 'asc')->get();
        }

        return view('home', compact('user', 'classes'));
    }
}

Next resources/views/home.blade.php:

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">{{$user->user_type}} {{ __('Dashboard') }}</div>

                    <div class="card-body">
                        @if (session('status'))
                            <div class="alert alert-success" role="alert">
                                {{ session('status') }}
                            </div>
                        @endif
                        @if($user->user_type === "Student")
                            <h3>These are the ongoing classes available on the system</h3>
                            @foreach($classes as $key=>$class)
                                <a href="{{route('classroom', ['id' => $class->id])}}">{{$key + 1}}. {{$class->name}}</a>
                                <br />
                            @endforeach
                        @else
                            <h4>Welcome {{$user->name}}. Fill the form below to create a class</h4>
                            <form method="POST" action="{{ route('create_class') }}">
                                @csrf

                                <div class="form-group row">
                                    <label for="name" class="col-md-12 col-form-label">{{ __('Class Name') }}</label>

                                    <div class="col-md-6">
                                        <input id="name" type="text"
                                               class="form-control @error('name') is-invalid @enderror" name="name"
                                               value="{{ old('name') }}" required autocomplete="name" autofocus>

                                        @error('name')
                                        <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                        @enderror

                                    </div>
                                </div>

                                <div class="form-group row mb-0">
                                    <div class="col-md-6">
                                        <button type="submit" class="btn btn-primary">
                                            {{ __('Create Class') }}
                                        </button>
                                    </div>
                                </div>
                            </form>

                        @endif

                    </div>
                </div>
            </div>
        </div>
    </div>

@endsection

This either provides a form to create a class if the user is a teacher or a virtual class list if the user is a student.

Setting up Our Client and Streaming Live Video-Audio Feed

In order to set up the client (which would be using the web SDK), we’ll need to create a blade file called resources/views/classroom.blade.php to match the showClassroom() method on our SessionsController. The file would need to have a link to the SDK’s CDN. Our frontend should look just like this:

<html>
    <head>
        <title> OpenTok Getting Started </title>
        <style>
          body, html {
              background-color: gray;
              height: 100%;
          }

          #videos {
              position: relative;
              width: 100%;
              height: 100%;
              margin-left: auto;
              margin-right: auto;
          }

          #subscriber {
              position: absolute;
              left: 0;
              top: 0;
              width: 100%;
              height: 100%;
              z-index: 10;
          }

          #publisher {
              position: absolute;
              width: 360px;
              height: 240px;
              bottom: 10px;
              left: 10px;
              z-index: 100;
              border: 3px solid white;
              border-radius: 3px;
          }

        </style>

        <script src="https://static.opentok.com/v2/js/opentok.min.js"></script>

    </head>

    <body>
        <div id="videos">
            <div id="subscriber"></div>
            <div id="publisher"></div>
        </div>

        <script type="text/javascript">
            var session;
            var connectionCount = 0;
            var apiKey = "{{env('VONAGE_API_KEY')}}";
            var sessionId = "{{$sessionId}}";
            var token = "{{$token}}";
            var publisher;

            function connect() {

                // Replace apiKey and sessionId with your own values:

                session = OT.initSession(apiKey, sessionId);
                session.on("streamCreated", function (event) {
                    console.log("New stream in the session: " + event.stream.streamId);
                    session.subscribe(event.stream, 'subscriber', {
                        insertMode: 'append',
                        width: '100%',
                        height: '100%'
                    });
                });

                session.on({
                    connectionCreated: function (event) {
                        connectionCount++;
                        alert(connectionCount + ' connections.');
                    },
                    connectionDestroyed: function (event) {
                        connectionCount--;
                        alert(connectionCount + ' connections.');
                    },
                    sessionDisconnected: function sessionDisconnectHandler(event) {
                        // The event is defined by the SessionDisconnectEvent class
                        alert('Disconnected from the session.');
                        document.getElementById('disconnectBtn').style.display = 'none';
                        if (event.reason == 'networkDisconnected') {
                            alert('Your network connection terminated.')
                        }
                    }
                });

                var publisher = OT.initPublisher('publisher', {
                    insertMode: 'append',
                    width: '100%',
                    height: '100%'
                }, error => {
                    if (error) {
                        alert(error.message);
                    }
                });

                // Replace token with your own value:
                session.connect(token, function (error) {
                    if (error) {
                        alert('Unable to connect: ', error.message);
                    } else {
                        // document.getElementById('disconnectBtn').style.display = 'block';
                        alert('Connected to the session.');
                        connectionCount = 1;

                        if (session.capabilities.publish == 1) {
                            session.publish(publisher);
                        } else {

                            alert("You cannot publish an audio-video stream.");
                        }
                    }
                });
            }
            connect();

        </script>
    </body>
</html>

To test the application, run the following command your project root directory.

php artisan serve

Open http://localhost:8000/ in your browser and click on the login menu.

If done correctly, you should log in with a teacher account and a student account and exchange audio and video feed.

Log in with a teacher account and a student account and exchange audio and video feed
Log in with a teacher account and a student account and exchange audio and video feed

When connected with a student, you should see this screen.

Connected with a student
Connected with a student

Conclusion

Real-time applications are amazing. They're bringing the world together, and now when the world is growing remote, demand for such applications is high.

Working with WebRTC and WebSockets for real-time communication can be cumbersome, but Vonage API provides easy helper methods for a wide range of use-cases. This article demonstrated how to set up a video feed using Laravel. You can clone the repository here.