$ composer create-project symfony/website-skeleton symfony-react-video-chat
symfony-react-video-chat
folder in your development folder. Alternatively, you can use Symfony Installer to set up your project using the following instructions.$ cd symfony-react-video-chat $ composer require twilio/sdk
twilio/sdk
: A Twilio PHP SDK that will facilitate interaction between the Twilio API and our applicationTWILIO_ACCOUNT_SID
from your dashboard and keep it safe:TWILIO_ACCOUNT_SID
, API_KEY_SID
, and API_KEY_SECRET
as obtained from the Twilio console. The .env file should have the content displayed below:TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID TWILIO_API_KEY_SID=YOUR_API_KEY TWILIO_API_KEY_SECRET=YOUR_API_SECRET
$ php bin/console make:controller TokenController
<?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Twilio\Jwt\AccessToken; use Twilio\Jwt\Grants\VideoGrant; class TokenController extends AbstractController { /** * @Route("/", name="token") */ public function index() { return $this->render('token/index.html.twig', [ 'controller_name' => 'TokenController', ]); } /** * @param Request $request * @return \Symfony\Component\HttpFoundation\JsonResponse * @Route("access_token", name="access_token", methods={"POST"}) */ public function generate_token(Request $request) { $accountSid = getenv('TWILIO_ACCOUNT_SID'); $apiKeySid = getenv('TWILIO_API_KEY_SID'); $apiKeySecret = getenv('TWILIO_API_KEY_SECRET'); $identity = uniqid(); $roomName = json_decode($request->getContent()); // Create an Access Token $token = new AccessToken( $accountSid, $apiKeySid, $apiKeySecret, 3600, $identity ); // Grant access to Video $grant = new VideoGrant(); $grant->setRoom($roomName->roomName); $token->addGrant($grant); return $this->json(['token' => $token->toJWT()], 200); } }
index()
method to render the content of the token/index.html.twig on the homepage route. The video chat will be visible on this page at the completion of this tutorial.generate_token()
is created. In this method, we accessed the environment variables defined in the .env file earlier and passed them as a part of the required parameters for a new instance of the AccessToken()
method from theTwilio SDK.{% extends 'base.html.twig' %} {% block title %} Symfony React Video Chat !{% endblock %} {% block body %} <div id="root"></div> {% endblock %}
<div>
with an id
of root
.$ symfony server:start
localhost:8000
in your preferred web browser. You’ll notice an empty page. Leave the server running for now. We will revisit it later.Composer
to install Symfony Webpack Encore by running the command below:$ composer require symfony/webpack-encore-bundle
$ yarn install
assets
folder, and add the node_modules folder to the .gitignore file.$ yarn add react react-dom @babel/preset-react --dev $ yarn add axios twilio-video --save $ yarn install
var Encore = require('@symfony/webpack-encore'); // Manually configure the runtime environment if not already configured yet by the "encore" command. // It's useful when you use tools that rely on webpack.config.js file. if (!Encore.isRuntimeEnvironmentConfigured()) { Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev'); } Encore // directory where compiled assets will be stored .setOutputPath('public/build/') // public path used by the web server to access the output path .setPublicPath('/build') // only needed for CDN's or sub-directory deploy //.setManifestKeyPrefix('build/') /* * ENTRY CONFIG * * Add 1 entry for each "page" of your app * (including one that's included on every page - e.g. "app") * * Each entry will result in one JavaScript file (e.g. app.js) * and one CSS file (e.g. app.css) if your JavaScript imports CSS. */ .addEntry('app', './assets/app.js') // When enabled, Webpack "splits" your files into smaller pieces for greater optimization. .splitEntryChunks() // will require an extra script tag for runtime.js // but, you probably want this, unless you're building a single-page app .enableSingleRuntimeChunk() /* * FEATURE CONFIG * * Enable & configure other features below. For a full * list of features, see: * https://symfony.com/doc/current/frontend.html#adding-more-features */ .cleanupOutputBeforeBuild() .enableBuildNotifications() .enableSourceMaps(!Encore.isProduction()) // enables hashed filenames (e.g. app.abc123.css) .enableVersioning(Encore.isProduction()) // enables @babel/preset-env polyfills .configureBabelPresetEnv((config) => { config.useBuiltIns = 'usage'; config.corejs = 3; }) // enables Sass/SCSS support //.enableSassLoader() // uncomment if you use TypeScript //.enableTypeScriptLoader() // uncomment to get integrity="..." attributes on your script & link tags // requires WebpackEncoreBundle 1.4 or higher //.enableIntegrityHashes(Encore.isProduction()) // uncomment if you're having problems with a jQuery plugin //.autoProvidejQuery() // uncomment if you use API Platform Admin (composer req api-admin) .enableReactPreset() //.addEntry('admin', './assets/js/admin.js') ; module.exports = Encore.getWebpackConfig();
.enableReactPreset()
method.import React, { useState } from "react"; import axios from "axios"; import Video from "twilio-video"; const Chat = () => { const [roomName, setRoomName] = useState(''); const [hasJoinedRoom, setHasJoinedRoom] = useState(false); const joinChat = event => { event.preventDefault(); if (roomName) { axios.post('/access_token', { roomName }, ).then((response) => { connectToRoom(response.data.token); setHasJoinedRoom(true); setRoomName(''); }).catch((error) => { console.log(error); }) } else { alert("You need to enter a room name") } }; return( <div className="container"> <div className={"col-md-12"}> <h1 className="text-title">Symfony React Video Chat</h1> </div> <div className="col-md-6"> <div className={"mb-5 mt-5"}> {!hasJoinedRoom && ( <form className="form-inline" onSubmit={joinChat}> <input type="text" name={'roomName'} className={"form-control"} id="roomName" placeholder="Enter a room name" value={roomName} onChange={event => setRoomName(event.target.value)}/> <button type="submit" className="btn btn-primary">Join Room</button> </form> )} </div> <div id="video-chat-window"></div> </div> </div> ) }; export default Chat;
joinChat()
method will be called and a POST HTTP request will be sent to the access_token
endpoint. Upon a successful response, the retrieved token
will be used to connect to a live video chat session.connectToRoom()
immediately after the joinChat()
function:... const Chat = () => { ... const connectToRoom = (token) => { const { connect, createLocalVideoTrack } = Video; let connectOption = { name: roomName }; connect( token, connectOption).then(room => { console.log(`Successfully joined a Room: ${room}`); const videoChatWindow = document.getElementById('video-chat-window'); createLocalVideoTrack().then(track => { videoChatWindow.appendChild(track.attach()); }); room.on('participantConnected', participant => { console.log(`Participant "${participant.identity}" connected`); participant.tracks.forEach(publication => { if (publication.isSubscribed) { const track = publication.track; videoChatWindow.appendChild(track.attach()); } }); participant.on('trackSubscribed', track => { videoChatWindow.appendChild(track.attach()); }); }); }, error => { console.error(`Unable to connect to Room: ${error.message}`); }); }; ... }; export default Chat;
token
obtained from the backend API and used it to set up a connection to the chat room. We also created a local video track and appended it to a chat window where it will be displayed.room.on()
method displays their name within the chat window.root
. Open the assets/app.js file and replace its content with:import React from 'react'; import ReactDOM from "react-dom"; import Chat from './js/Chat'; import './styles/app.css' ReactDOM.render(<Chat/>, document.getElementById("root"));
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>{% block title %}Welcome!{% endblock %}</title> </head> <body> {% block body %}{% endblock %} {% block stylesheets %} {{ encore_entry_link_tags('app') }} {% endblock %} {% block javascripts %} {{ encore_entry_script_tags('app') }} {% endblock %} </body> </html>
@import url(https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css);
$ php bin/console server:run
$ yarn encore dev --watch