4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TwilioFlexにビデオ通話(Twilio Video)機能を備えてみた

TwilioFlexにビデオ通話を下のイメージのように、備えたためそのレポートです。
スクリーンショット 2021-12-02 20.59.28.png

TwilioFunctionsを利用して、バックエンドを作成しました。
主な役割としては、トークンの発行とビデオ通話確認ようの画面を作成しました。

TwilioFlexのプラグインを用いて図のような画面を作成して
ビデオ通話ができるようにしました。

作り込みの甘い部分がありますが、とりあえず使えるかと思います。
備忘録的なところがあるため、雑ですみません。

指摘等や疑問があればコメントにて対応したいと思います。

TwilioFunctionsを利用して、トークンの作成

初めにTwilioFunctionsを利用して、ビデオ通話用のトークンを作成した。
TwilioFunctionsの使い方については他の記事を参考にしてほしい。

フォルダ構成は下記の感じ
スクリーンショット 2021-12-02 21.08.19.png

下記がバックグラウンド処理となる。

functions/create.js
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「*****************」は読み替えてほしい。

assets/video.js
(() => {
    '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以外からビデオを確認するためのフロント画面処理

assets/index.html
<!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>

環境設定は下記のように行う。
スクリーンショット 2021-12-02 21.14.43.png

TwilioFlexのプラグイン作成

TwilioFlexのプラグインの使い方については他の記事を参考にしてほしい。

今回のソースはReactのバーションアップをした為、下記手順が必須となります。
https://www.twilio.com/docs/flex/developer/plugins/react-versions

画面整形のため、material-uiも利用しています。

フォルダ構成は下記の感じ
スクリーンショット 2021-12-02 21.19.27.png

今回の変更と作成ファイルは以下のものとなります。
「VideoViewPlugin」として作成しました。

作成ファイル

  • VideoSideNav.js
  • VideoView.js

変更ファイル

  • VideoViewPlugin.js

下記のファイルはサイドのパーようのソースです。

src/components/VideoSideNav.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「*****************」は読み替えてほしい。

src/components/VideoSideNav.js
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

下記はプラグインのメインファイルで、もともとあるファイルを書き換えました。

src/VideoViewPlugin.js
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>
    )

  }
}
4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?