TwilioFlexにビデオ通話(Twilio Video)機能を備えてみた
TwilioFlexにビデオ通話を下のイメージのように、備えたためそのレポートです。
TwilioFunctionsを利用して、バックエンドを作成しました。
主な役割としては、トークンの発行とビデオ通話確認ようの画面を作成しました。
TwilioFlexのプラグインを用いて図のような画面を作成して
ビデオ通話ができるようにしました。
作り込みの甘い部分がありますが、とりあえず使えるかと思います。
備忘録的なところがあるため、雑ですみません。
指摘等や疑問があればコメントにて対応したいと思います。
TwilioFunctionsを利用して、トークンの作成
初めにTwilioFunctionsを利用して、ビデオ通話用のトークンを作成した。
TwilioFunctionsの使い方については他の記事を参考にしてほしい。
下記がバックグラウンド処理となる。
const AccessToken = require('twilio').jwt.AccessToken;
const VideoGrant = AccessToken.VideoGrant;
exports.handler = function(context, event, callback) {
// Create a custom Twilio Response
const response = new Twilio.Response();
// Set the CORS headers to allow Flex to make an error-free HTTP request
// to this Function
response.appendHeader('Access-Control-Allow-Origin', '*');
response.appendHeader('Access-Control-Allow-Methods', 'OPTIONS, POST, GET');
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');
const identity = event.username ? event.username : "noname";
const accessToken = new AccessToken(context.ACCOUNT_SID,context.API_KEY, context.API_SECRET);
accessToken.identity = identity;
// Grant access to Video
let grant = new VideoGrant();
grant.room = event.roomname ? event.roomname : 'default' ;
accessToken.addGrant(grant);
const data = {identity: identity,token: accessToken.toJwt(), };
response.appendHeader('Content-Type', 'application/json');
response.setBody(data);
return callback( null, response );
};
フロントの処理は下記となる。
URL「*****************」は読み替えてほしい。
(() => {
'use strict';
let username = 'gest';
let roomname = 'unoh99';
const Video = Twilio.Video;
let videoRoom, localStream;
const urlParam = location.search.substring(1);
if(urlParam) {
const param = urlParam.split('&');
const paramArray = [];
// 用意した配列にパラメータを格納
for (let i = 0; i < param.length; i++) {
const paramItem = param[i].split('=');
paramArray[paramItem[0]] = paramItem[1];
}
if (paramArray.username) {
username = paramArray.username;
}
if(paramArray.roomname){
roomname = paramArray.roomname;
}
}
// プレビュー画面の表示
navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then(stream => {
//document.getElementById("myStream").srcObject = stream;
localStream = stream;
});
// ボタンの準備
const btnJoinRoom = document.getElementById("button-join");
const btnLeaveRoom = document.getElementById("button-leave");
btnJoinRoom.onclick = (() => {
// アクセストークンを取得
axios.get(`https://*****************.twil.io/create?username=${username}&roomname=${roomname}`)
.then(async body => {
const token = body.data.token;
console.log(token);
Video.connect(token, { name: roomname })
.then(room => {
console.log(`Connected to Room ${room.name}`);
videoRoom = room;
room.participants.forEach(participantConnected);
room.on('participantConnected', participantConnected);
room.on('participantDisconnected', participantDisconnected);
room.once('disconnected', error => room.participants.forEach(participantDisconnected));
btnJoinRoom.disabled = true;
btnLeaveRoom.disabled = false;
}).catch( (error) => { console.error(error); } );
}).catch( (error) => { console.error(error); } );
});
btnLeaveRoom.onclick = (() => {
videoRoom.disconnect();
console.log(`Disconnected to Room ${videoRoom.name}`);
btnJoinRoom.disabled = false;
btnLeaveRoom.disabled = true;
});
})();
const participantConnected = (participant) => {
console.log(`Participant ${participant.identity} connected'`);
const div = document.createElement('div');
div.id = participant.sid;
div.classList.add('card');
div.classList.add('col');
div.innerHTML = '<div class="card-header">'+ participant.identity + '</div>';
participant.on('trackSubscribed', track => trackSubscribed(div, track));
participant.on('trackUnsubscribed', trackUnsubscribed);
participant.tracks.forEach(publication => {
if (publication.isSubscribed) {
trackSubscribed(div, publication.track);
}
});
document.getElementById("Stream").appendChild(div);
}
const participantDisconnected = (participant) => {
console.log(`Participant ${participant.identity} disconnected.`);
document.getElementById(participant.sid).remove();
}
const trackSubscribed = (div, track) => {
div.appendChild(track.attach());
}
const trackUnsubscribed = (track) => {
track.detach().forEach(element => element.remove());
}
下記はFlex以外からビデオを確認するためのフロント画面処理
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<title>Twilio-ビデオ通話</title>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-md">
<h1 class="navbar-brand" >Twilio ビデオ通話</h1>
</div>
</nav>
<main class="container">
<div id="room-controls" class="row">
<div class="col">
<button class="btn btn-primary w-100" id="button-join">ビデオ通話の開始</button>
</div>
<div class="col">
<button class="btn btn-danger w-100" id="button-leave" disabled>ビデオ通話の終了</button>
</div>
</div>
<div class="container">
<div class="row" id="Stream">
</div>
</div>
<!-- <video id="myStream" autoplay muted="true"></video> -->
</main>
</body>
<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
<!-- <script src="//media.twiliocdn.com/sdk/js/video/releases/2.3.0/twilio-video.min.js"></script> -->
<!-- <script src="//media.twiliocdn.com/sdk/js/video/v1/twilio-video.min.js"></script> -->
<script src="//sdk.twilio.com/js/video/releases/2.18.1/twilio-video.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="./video.js?version=1.00"></script>
</html>
TwilioFlexのプラグイン作成
TwilioFlexのプラグインの使い方については他の記事を参考にしてほしい。
今回のソースはReactのバーションアップをした為、下記手順が必須となります。
https://www.twilio.com/docs/flex/developer/plugins/react-versions
画面整形のため、material-uiも利用しています。
今回の変更と作成ファイルは以下のものとなります。
「VideoViewPlugin」として作成しました。
作成ファイル
- VideoSideNav.js
- VideoView.js
変更ファイル
- VideoViewPlugin.js
下記のファイルはサイドのパーようのソースです。
import React from "react";
import { SideLink , Actions } from "@twilio/flex-ui";
const VideoSideNav = ({ activeView }) => {
function navigate() {
Actions.invokeAction( 'NavigateToView' , {viewName : 'video-view'} );
}
return (
<SideLink
showLabel = { true }
icon="Video"
iconActive="VideoBold"
isActive={activeView == 'video-view'}
onClick={navigate}
>
Video View
</SideLink>
);
}
export default VideoSideNav;
下記のファイルはメイン画面のソースです。
URL「*****************」は読み替えてほしい。
import React, { useState ,useEffect} from 'react';
import axios from 'axios'
import Twiliovideo from "twilio-video"
import {Container , Paper, Typography, Button , Grid ,Card , CardHeader , CardContent} from '@material-ui/core/';
import {Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions} from '@material-ui/core/';
import {Menu , MenuItem} from '@material-ui/core/';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
icon: {
marginRight: theme.spacing(2),
},
heroContent: {
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(8, 0, 6),
},
heroButtons: {
marginTop: theme.spacing(4),
},
cardGrid: {
paddingTop: theme.spacing(8),
paddingBottom: theme.spacing(8),
},
card: {
height: '100%',
display: 'flex',
flexDirection: 'column',
},
cardMedia: {
paddingTop: '56.25%', // 16:9
},
cardContent: {
flexGrow: 1,
},
footer: {
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(6),
},
}));
const VideoView = ({flex,manager}) => {
const classes = useStyles();
const [roominfo, setRoominfo] = useState(false);
const [username, setUsername] = useState(manager.workerClient.attributes.full_name);
const [roomname, setRoomName] = useState(manager.user.identity);
const [token, setToken] = useState(null);
const [videoroom, setVideoroom] = useState(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const handleMenuClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
const handleClickOpen = () => {
setRoominfo(true);
};
const handleClose = () => {
setRoominfo(false);
};
console.log("デバックポイント");
//console.log(flex);
console.log(manager);
const trackSubscribed = (div, track) => {
const child = div.appendChild(track.attach());
if (track.kind === "video") {
child.classList.add("video-style");
}
}
const trackUnsubscribed = (track) => {
track.detach().forEach(element => element.remove());
}
const participantConnected = (participant) => {
console.log(`connect!`);
console.log(`Participant ${participant.identity} connected'`);
// 参加者を表示する
const div = document.createElement("div");
div.id = participant.sid;
div.classList.add("remote-video");
// 参加者のトラックが届いたとき
participant.on('trackSubscribed', (track) => trackSubscribed(div, track));
// 参加者の画像を表示
const videoZone = document.getElementById('video-zone');
if(videoZone){
videoZone.appendChild(div);
}
}
const participantDisconnected = (participant) => {
console.log(`Participant ${participant.identity} disconnected.`);
if(document.getElementById(participant.sid)){
document.getElementById(participant.sid).remove();
}
}
const videoStart = () => {
axios.get('https:/********************.twil.io/create?username='+username+'&roomname=' + roomname).then(function (res) {
console.log( res.data.token );
setToken(res.data.token);
try {
Twiliovideo.connect(res.data.token, {
name: roomname,
}).then((room) => {
console.log(`Connected to Room ${room.name}`);
setVideoroom(room);
// すでに入室している参加者を表示
room.participants.forEach(participantConnected);
// 誰かが入室してきたときの処理
room.on("participantConnected", participantConnected);
room.on('participantDisconnected', participantDisconnected);
room.once('disconnected', error => room.participants.forEach(participantDisconnected));
})
setAnchorEl(null);
} catch (error) {
console.log(error);
}
});
}
const vidoeEnd = () => {
videoroom.disconnect();
console.log(`Disconnected to Room ${videoroom.name}`);
setAnchorEl(null);
}
return (
<React.Fragment>
<Container component="main" maxWidth="xl">
<Paper elevation={0} >
<Grid container justifyContent="space-between" >
<Grid item xs={9}>
<Typography component="h5" variant="h5" color="textPrimary" gutterBottom>
ビデオ通話コンソール
</Typography>
</Grid>
<Grid item xs={1}>
<Button aria-controls="simple-menu" aria-haspopup="true" onClick={handleMenuClick}>
Menu
</Button>
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={videoStart}>通話開始</MenuItem>
<MenuItem onClick={vidoeEnd}>通話終了</MenuItem>
<MenuItem onClick={handleClickOpen}>ご案内URL</MenuItem>
</Menu>
</Grid>
</Grid>
<Grid container spacing={1} justifyContent="center">
<Grid item id="video-zone">
</Grid>
</Grid>
</Paper>
</Container>
<Dialog
open={roominfo}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">お客様ご案内</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{'https://*********.twil.io/index.html?username=gest&roomname=' + roomname}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary" autoFocus>
閉じる
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
)
}
export default VideoView
下記はプラグインのメインファイルで、もともとあるファイルを書き換えました。
import React from 'react';
import { VERSION , View } from '@twilio/flex-ui';
import { FlexPlugin } from 'flex-plugin';
import VideoSideNav from './components/VideoSideNav';
import VideoView from './components/VideoView'
const PLUGIN_NAME = 'VideoViewPlugin';
export default class VideoViewPlugin extends FlexPlugin {
constructor() {
super(PLUGIN_NAME);
}
/**
* This code is run when your plugin is being started
* Use this to modify any UI components or attach to the actions framework
*
* @param flex { typeof import('@twilio/flex-ui') }
* @param manager { import('@twilio/flex-ui').Manager }
*/
async init(flex, manager) {
flex.SideNav.Content.add(
<VideoSideNav key="video-side-nav"></VideoSideNav>
);
flex.ViewCollection.Content.add(
<View name='video-view' key='video-view'>
<VideoView flex={flex} manager={manager}></VideoView>
</View>
)
}
}