😶‍🌫️

Amazon Chimeを使ってMTGツールをローカルで作る

2024/07/29に公開

概要

Amazon Chimeを使用してローカルでビデオ会議ツールを作成してみました。
以前からWebRTCに興味はありましたが、なかなか触る機会がなかったため、まずはSDKを通してビデオ会議ツールを作成してみます。
今回はAWSが公式で出してくれている、構成を参考にしつつ、動くものを作りたいと思います。
次回のブログくらいでログイン機能などを追加して、もう少し汎用的にしつつ、実際のAWSのサービスに載せていきたいと思います。

GitHub: https://github.com/hosimesi/code-for-techblogs/tree/main/aws_chime_django_local

検証環境

以下の検証環境を用意しました。

チップ: Apple M3 Max
メモリ: 64 GB

ディレクトリ構成

# tree --gitignore
.
├── README.md
├── bundle
│   ├── package.json
│   ├── src
│   │   └── meeting.js
│   ├── webpack.config.js
│   └── yarn.lock
├── compose.yaml
├── docker
│   └── Dockerfile
├── pyproject.toml
├── requirements-dev.lock
├── requirements.lock
└── src
    ├── aws_chime_django
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── manage.py
    └── video_meeting_app
        ├── __init__.py
        ├── admin.py
        ├── apps.py
        ├── management
        │   └── commands
        │       └── seed_users.py
        ├── migrations
        │   ├── 0001_initial.py
        │   └── __init__.py
        ├── models.py
        ├── static
        │   ├── dist
        │   │   ├── meeting.js
        │   │   └── meeting.js.LICENSE.txt
        │   └── index.js
        ├── templates
        │   ├── index.html
        │   └── meeting.html
        ├── tests.py
        ├── urls.py
        └── views.py

Amazon Chimeとは

https://aws.amazon.com/jp/chime/

Amazon Chime は、組織の内外を問わず会議、チャット、業務上の電話を単一のアプリケーションで実現する通信サービスです。
Amzaon ChimeにはSDKが用意されており、アプリケーションに組み込みやすくなっています。

事前準備

今回はryeでpython環境を用意します。必要なライブラリは以下の通りです。

$ cd path/to/your/app
$ rye init                                                        
$ rye sync                                                       
$ rye run python -V                                                          
Python 3.12.3
$ rye add mysqlclient django boto3
$ rye sync

アプリケーション実装

まず、ローカルの開発環境をDjangoDockerで作っていきます。
デフォルトではsqliteを使用しますが、今後のデプロイなどを考えてMySQLを使用しておきます。
また今回は簡単化のため、ログイン機能などは省略し適当なseed userを使って会議に参加して行きます。

Dockerの準備

ローカルではDocker環境で動かすため、Dockerfileとcompose.yamlを作成しておきます。DjangoアプリケーションとMySQLの2つのコンテナを準備します。
Dockerfileは以下のようになります。

FROM python:3.12-slim as base


RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    libmagic1=1:5.44-3 \
    wget=1.21.3-* \
    python3-dev \
    default-libmysqlclient-dev \
    build-essential \
    pkg-config \
    && wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.4.24/grpc_health_probe-linux-amd64 \
    && chmod +x /bin/grpc_health_probe \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
ENV PYTHONPATH=/app

COPY requirements.lock /app/requirements.lock

RUN pip install --no-cache-dir --upgrade pip==24.0 \
    && pip install --no-cache-dir -r requirements.lock

COPY src /app/src

# ============== local =============
FROM base as local
COPY requirements-dev.lock /app/requirements-dev.lock

RUN pip install --no-cache-dir --upgrade pip==24.0 \
    && pip install --no-cache-dir -r requirements-dev.lock

compose.yamlは以下のようになります。

services:
  db:
    image: mysql:8.4
    ports:
      - "3306:3306"
    container_name: chime_db
    volumes:
      - .local/mysql:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: chime_db
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci

  server:
    build:
      context: .
      dockerfile: docker/Dockerfile
      target: local
    platform: linux/amd64
    container_name: chime_video_meeting
    ports:
      - "8000:8000"
    volumes:
      - .:/app
      - ~/.aws:/root/.aws
    command: python src/manage.py runserver 0.0.0.0:8000
    tty: true
    expose:
      - 8000
    environment:
      - AWS_PROFILE=<your-aws-profile>
    depends_on:
      - db

Djangoの準備

Djangoの初期化

まず、Djangoのプロジェクトを作成します。

$ cd src
$ rye run django-admin startproject aws_chime_django

その後、appを作ります。
まず、Djangoのプロジェクトを作成します。

$ rye run python manage.py startapp vido_meeting_app

その後、Docker内で動かしておきます。

$ docker compose up --build

テーブルの作成

ビデオ会議を実現するにあたってMeetingテーブルとUserテーブルとAttendeeテーブルを準備します。
UserテーブルはUserを管理しますが、ここではseed値を入れるので特に大きな役割ではなく、ミーティングへの参加者の重複をなくすために使用します。
Meetingテーブルはミーティングの管理するために使用し、参加者などの情報を持ちます。上記を加味して以下のように定義します。
Attendeeテーブルは参加者の管理に使用します。ChimeはUserが会議に参加するごとにレスポンスを返すので、それらを保持するようにテーブルを作成します。

# aws_chime_django_local/src/vido_meeting_app/models.py
import uuid

from django.db import models


class User(models.Model):
    user_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=255)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name


class Meeting(models.Model):
    meeting_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    title = models.CharField(max_length=255)
    attendees = models.ManyToManyField(
        User, through="Attendance", related_name="meetings"
    )
    created_at = models.DateTimeField(auto_now_add=True)
    created_by = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="created_meetings"
    )
    updated_at = models.DateTimeField(auto_now=True)
    updated_by = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="updated_meetings"
    )
    response = models.JSONField(
        default=dict,
        blank=True,
        null=True,
        editable=False,
        help_text="Response from Chime API",
    )


class Attendance(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    meeting = models.ForeignKey(Meeting, on_delete=models.CASCADE)
    joined_at = models.DateTimeField(auto_now_add=True)
    response = models.JSONField(
        default=dict,
        blank=True,
        null=True,
        editable=False,
        help_text="Response from Chime API",
    )

    def __str__(self):
        return f"{self.user.name} - {self.meeting.title}"

次にprojectのsettings.pyにアプリケーションの設定を入れていきます。今回はローカル環境のみなので直書きしていきます。

# aws_chime_django_local/src/aws_chime_django/settings.py
ALLOWED_HOSTS = ['0.0.0.0', 'localhost', '127.0.0.1']

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    "video_meeting_app",
]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'chime_db',
        'USER': 'user',
        'PASSWORD': 'password',
        'HOST': 'db',
        'PORT': '3306'
    }
}

DBをマイグレーションしてテーブルを作成します

$ docker exec -it chime_video_meeting python src/manage.py migrate

テーブルが作成されていればOKです。

次に、Seedデータを入れます。
Djangoではカスタムコマンドが定義できるのでSeedを入れるカスタムコマンドを作成します。

# video_meeting_app/management/commands/seed_users.py
from django.core.management.base import BaseCommand
from video_meeting_app.models import User


class Command(BaseCommand):
    help = "Create random users"

    def add_arguments(self, parser):
        parser.add_argument(
            "total", type=int, help="Indicates the number of users to be created"
        )

    def handle(self, *args, **kwargs):
        total = kwargs["total"]
        for i in range(total):
            User.objects.create(name=f"user{i+1}")
        self.stdout.write(self.style.SUCCESS(f"{total} users were created!"))
$ docker exec -it chime_video_meeting python src/manage.py seed_users 9

CLI or ツールを使ってテーブルにレコードが入っていることを確認できればOKです。

Viewの作成

次にDjangoのViewを作って行きます。
まずindex.htmlを作成します。
ここでは簡単に以下のようなhtmlを作成しておき、後ほどこちらを修正して行きます。

<!DOCTYPE html>
<html lang="en">

<head>
    <title>Amazon Chime Video Meeting</title>
</head>

<body>
    <h1>Amazon Chime Video Meeting</h1>
</body>

</html>

そして、projectのurls.pyにvideo_meeting_appのurlを追加しておきます。

# aws_chime_django_local/src/aws_chime_django/urls.py
"""
URL configuration for aws_chime_django project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include("video_meeting_app.urls"))
]

そして、app側にもurls.pyとviews.pyを準備していきます。

# aws_chime_django_local/src/video_meeting_app/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("health/", views.health_check, name="health_check"),
    path("api/users/", views.UserListView.as_view(), name="user-list"),
    path("api/meetings/", views.MeetingListView.as_view(), name="meeting-list"),
    path("api/meetings/create/", views.MeetingCreateView.as_view(), name="meeting-create"),
    path(
        "api/meetings/<uuid:meeting_id>/delete/",
        views.MeetingDeleteView.as_view(),
        name="meeting-delete",
    ),
    path("meeting/", views.meeting, name="meeting"),
]
# aws_chime_django_local/src/video_meeting_app/views.py
import json
import uuid

import boto3
from django.db.models import F
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, render
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt

from .models import Attendance, Meeting, User


def health_check(request):
    return JsonResponse({"status": "success"})


def index(request):
    return render(request, "index.html")

def meeting(request):
    meeting_id = request.GET.get('meetingId')
    user_id = request.GET.get('userId')

    meeting = Meeting.objects.get(meeting_id=meeting_id)
    attendee = Attendance.objects.get(meeting=meeting, user__user_id=user_id)

    return render(request, "meeting.html", {
        'meeting': json.dumps(meeting.response),
        'attendee': json.dumps(attendee.response),
    })


@method_decorator(csrf_exempt, name="dispatch")
class UserListView(View):
    def get(self, request):
        users = list(User.objects.values())
        return JsonResponse(users, safe=False)


@method_decorator(csrf_exempt, name="dispatch")
class MeetingListView(View):
    def get(self, request):
        meetings = Meeting.objects.annotate(
            created_by_name=F("created_by__name"),
        )
        meetings_list = []
        for meeting in meetings:
            attendees_list = []
            for attendee in Attendance.objects.filter(meeting=meeting):
                attendee_dict = {
                    "user_id": str(attendee.user.user_id),
                    "name": attendee.user.name,
                }
                attendees_list.append(attendee_dict)
            meeting_dict = {
                "meeting_id": str(meeting.meeting_id),
                "title": meeting.title,
                "created_by_id": meeting.created_by_id,
                "created_by_name": meeting.created_by_name,
                "created_at": meeting.created_at.strftime("%Y-%m-%d %H:%M:%S"),
                "updated_by_id": meeting.updated_by_id,
                "attendees": attendees_list,
            }
            meetings_list.append(meeting_dict)
        return JsonResponse(meetings_list, safe=False)


@method_decorator(csrf_exempt, name="dispatch")
class MeetingCreateView(View):
    def post(self, request):
        meeting_data = json.loads(request.body)
        created_by_id = meeting_data.get("created_by")
        if not created_by_id or created_by_id == "undefined":
            return JsonResponse(
                {"status": "error", "message": "Invalid user_id"}, status=400
            )
        try:
            created_by = User.objects.get(user_id=created_by_id)
        except User.DoesNotExist:
            return JsonResponse(
                {"status": "error", "message": "User does not exist"}, status=400
            )
        attendees_ids = meeting_data.get("participants")
        if not attendees_ids or "undefined" in attendees_ids:
            return JsonResponse(
                {"status": "error", "message": "Invalid attendees_ids"}, status=400
            )
        try:
            attendees = User.objects.filter(user_id__in=attendees_ids)
        except User.DoesNotExist:
            return JsonResponse(
                {"status": "error", "message": "Attendee does not exist"}, status=400
            )
        client = boto3.client("chime-sdk-meetings")
        meeting_id = str(uuid.uuid4())
        response = client.create_meeting(
            ClientRequestToken=str(uuid.uuid4()),
            MediaRegion="ap-northeast-1",
            MeetingHostId=meeting_id,
            ExternalMeetingId=meeting_id,
        )
        meeting_data_model = {
            "title": meeting_data.get("title"),
            "meeting_id": meeting_id,
            "created_by": created_by,
            "updated_by": created_by,
            "response": response,
        }
        meeting = Meeting.objects.create(**meeting_data_model)
        for attendee in attendees:
            attendee_response = client.create_attendee(
                MeetingId=response["Meeting"]["MeetingId"],
                ExternalUserId=str(attendee.user_id),
            )
            Attendance.objects.create(
                user=attendee,
                meeting=meeting,
                response=attendee_response,
            )
        return JsonResponse(
            {"status": "success", "meeting_id": str(meeting.meeting_id)}
        )


@method_decorator(csrf_exempt, name="dispatch")
class MeetingDeleteView(View):
    def delete(self, request, meeting_id):
        meeting = get_object_or_404(Meeting, meeting_id=meeting_id)
        client = boto3.client("chime-sdk-meetings")
        client.delete_meeting(MeetingId=str(meeting.meeting_id))
        Attendance.objects.filter(meeting=meeting).delete()
        meeting.delete()
        return JsonResponse({"status": "success"})

http://localhost:8000/ で画面が見えたらOKです。
viewsでは、indexで表示するための会議一覧やユーザ一覧を提供します。
また、参加ボタンを押されたタイミングでその会議とユーザのChimeの認証情報を返しています。

画面の準備

Template編集

先ほど作成してindex.htmlをvideo会議ができるように修正していきます。また、video会議ができるようなmeeting.htmlも追加で作成して行きます。

# aws_chime_django_local/src/video_meeting_app/templates/index.html
<!DOCTYPE html>
<html lang="en">
{% load static %}

<head>
    <title>Amazon Chime SDK Readiness Checker</title>
</head>

<body>
    <div id="create meeting" class="flow text-center p-2">
        <form id="createMeetingForm">
            <label for="meetingName">ミーティング名:</label>
            <input type="text" id="meetingName" name="meetingName"><br>
            <label for="createdBy">作成者:</label>
            <select id="createdBy">
                {% for user in users %}
                <option value="{{ user.user_id }}">{{ user.name }}</option>
                {% endfor %}
            </select><br>
            <label>参加者:</label><br>
            <div id="participants">
                {% for user in users %}
                <input type="checkbox" id="participant{{ user.user_id }}" value="{{ user.user_id }}">
                <label for="participant{{ user.user_id }}">{{ user.name }}</label><br>
                {% endfor %}
            </div>
            <input type="submit" value="作成">
        </form>
        <video id="video-view" autoplay></video>
        <audio id="audio-view" autoplay></audio>
    </div>

    <div id="meetingTable">
        <h1>Meeting List</h1>
        <table>
            <tr>
                <th>会議 ID</th>
                <th>会議名</th>
                <th>作成者</th>
                <th>作成日時</th>
                <th>参加ユーザー選択</th>
                <th>参加ボタン</th>
                <th>削除ボタン</th>
            </tr>
        </table>
    </div>
    <script src="{% static 'index.js' %}"></script>
</body>

</html>
<!DOCTYPE html>
<html lang="en">
{% load static %}

<head>
    <title>Amazon Chime Meeting</title>
    <script>
        window.meeting = JSON.parse(`{{ meeting|safe }}`);
        window.attendee = JSON.parse(`{{ attendee|safe }}`);
    </script>
</head>
<body>
    <div>
        <div id="meeting title">
            <h1>Amazon Chime Meeting</h1>
        </div>
        <div id="video-view-div">
            <video id="video-view"></video>
        </div>
        <audio id="audio-view"></audio>
    </div>
    <script src="{% static 'dist/meeting.js' %}"></script>
</body>
</html>

JavaScript

JSはあまり詳しくないので、とりあえず動くものを簡単にフレームワークを使わず作ってみます。React等でもDjangoと組み合わせらるので、それらはまた別の機会に試して行きたいと思います。

# aws_chime_django_local/src/video_meeting_app/static/index.js
async function fetchUsers() {
    const response = await fetch('/api/users/');
    let users = await response.json();
    users = users.sort((a, b) => a.name.localeCompare(b.name));
    const createdBySelect = document.getElementById('createdBy');
    const participantsDiv = document.getElementById('participants');
    users.forEach(user => {
        const createdByOption = document.createElement('option');
        createdByOption.value = user.user_id;
        createdByOption.text = user.name;
        createdBySelect.appendChild(createdByOption);
        const participantCheckbox = document.createElement('input');
        participantCheckbox.type = 'checkbox';
        participantCheckbox.id = `participant${user.user_id}`;
        participantCheckbox.value = user.user_id;
        const participantLabel = document.createElement('label');
        participantLabel.htmlFor = participantCheckbox.id;
        participantLabel.appendChild(document.createTextNode(user.name));
        participantsDiv.appendChild(participantCheckbox);
        participantsDiv.appendChild(participantLabel);
        participantsDiv.appendChild(document.createElement('br'));
    });
}

document.getElementById('createMeetingForm').addEventListener('submit', async (event) => {
    event.preventDefault();
    const meetingName = document.getElementById('meetingName').value;
    const createdBy = document.getElementById('createdBy').value;
    const participants = Array.from(document.getElementById('participants').getElementsByTagName('input'))
        .filter(checkbox => checkbox.checked)
        .map(checkbox => checkbox.value);
    const meetingData = JSON.stringify({
        title: meetingName,
        created_by: createdBy,
        participants: participants
    });
    await fetch('/api/meetings/create/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: meetingData,
    });
});


async function joinMeeting(meetingId, userId) {
    const meetingUrl = new URL('/meeting/', window.location.href);
    meetingUrl.searchParams.set('meetingId', meetingId);
    meetingUrl.searchParams.set('userId', userId);
    window.open(meetingUrl.toString(), '_blank');
}

async function deleteMeeting(meetingId) {
    await fetch(`/api/meetings/${meetingId}/delete/`, {
        method: 'DELETE',
    });
}

function fetchMeetingsAndUpdateList() {
    fetch('/api/meetings/')
        .then(response => response.json())
        .then(meetings => {
            const table = document.querySelector('#meetingTable table');
            // Remove all rows except the header row
            while (table.rows.length > 1) {
                table.deleteRow(1);
            }
            // Add a new row for each meeting
            for (const meeting of meetings) {
                const row = table.insertRow(-1);
                row.insertCell(0).innerText = meeting.meeting_id;
                row.insertCell(1).innerText = meeting.title;
                row.insertCell(2).innerText = meeting.created_by_name;
                row.insertCell(3).innerText = meeting.created_at;
                const selectCell = row.insertCell(4);
                // Add a select element for the attendees
                const select = document.createElement('select');
                for (const attendee of meeting.attendees) {
                    const option = document.createElement('option');
                    option.value = attendee.user_id;
                    option.text = attendee.name;
                    select.appendChild(option);
                }
                selectCell.appendChild(select);

                // Join Action
                const joinCell = row.insertCell(5);
                const joinButton = document.createElement('button');
                joinButton.innerText = 'Join';
                joinButton.addEventListener('click', () => {
                    const selectedOption = select.options[select.selectedIndex];
                    joinMeeting(meeting.meeting_id, selectedOption.value);
                });
                joinCell.appendChild(joinButton);
                // Deelete Action
                const deleteCell = row.insertCell(6);
                const deleteButton = document.createElement('button');
                deleteButton.innerText = 'Delete';
                deleteButton.addEventListener('click', () => {
                    deleteMeeting(meeting.meeting_id);
                });
                deleteCell.appendChild(deleteButton);
            }
        }
    );
}

setInterval(fetchMeetingsAndUpdateList, 5000);
fetchMeetingsAndUpdateList();
fetchUsers();
# aws_chime_django_local/bundle/src/meeting.js
import * as ChimeSDK from 'amazon-chime-sdk-js';

window.addEventListener('DOMContentLoaded', async () => {
    // wait for the meeting and attendee to be set
    while (!window.meeting || !window.attendee) {
        await new Promise(resolve => setTimeout(resolve, 100));
    }

    const meeting = window.meeting;
    const attendee = window.attendee;

    const logger = new ChimeSDK.ConsoleLogger('ChimeMeetingLogs', ChimeSDK.LogLevel.INFO);
    const deviceController = new ChimeSDK.DefaultDeviceController(logger);
    const configuration = new ChimeSDK.MeetingSessionConfiguration(meeting, attendee);
    const meetingSession = new ChimeSDK.DefaultMeetingSession(configuration, logger, deviceController);


    meetingSession.audioVideo.setDeviceLabelTrigger(() => Promise.resolve(new MediaStream()));
    meetingSession.audioVideo.start();

    // Invoke devices
    meetingSession.audioVideo.setDeviceLabelTrigger(async () =>
        await navigator.mediaDevices.getUserMedia({ audio: true, video: true })
    );
    const audioInputDevices = await meetingSession.audioVideo.listAudioInputDevices();
    const audioOutputDevices = await meetingSession.audioVideo.listAudioOutputDevices();
    const videoInputDevices = await meetingSession.audioVideo.listVideoInputDevices();
    await meetingSession.audioVideo.startAudioInput(audioInputDevices[0].deviceId);
    await meetingSession.audioVideo.chooseAudioOutput(audioOutputDevices[0].deviceId);
    await meetingSession.audioVideo.startVideoInput(videoInputDevices[0].deviceId);



    const audioElement = document.getElementById('audio-view');
    const videoElement = document.getElementById('video-view');
    const videoElementTile = document.getElementById('video-view-div');
    meetingSession.audioVideo.bindAudioElement(audioElement);

    const observer = {
        videoTileDidUpdate: tileState => {

            if (tileState.localTile){
                meetingSession.audioVideo.bindVideoElement(tileState.tileId, videoElement);
            }else{
                if(!document.getElementById(tileState.tileId)){
                    const node = document.createElement("video");
                    node.id = tileState.tileId;
                    videoElementTile.appendChild(node);
                }
                const videoElementNew = document.getElementById(tileState.tileId);
                meetingSession.audioVideo.bindVideoElement(tileState.tileId, videoElementNew);
            }
        },
        videoTileWasRemoved: tileId => {
            if(document.getElementById(tileId)){
                const videoElementRemoved = document.getElementById(tileId);
                videoElementRemoved.remove();
            }
        }
    };

    meetingSession.audioVideo.addObserver(observer);
    meetingSession.audioVideo.startLocalVideoTile();
    meetingSession.audioVideo.start();


});

amazon-chime-sdk-jsを使うために、webpackを使ってバンドルしておきます。

$ yarn init
$ yarn add aws-sdk amazon-chime-sdk-js webpack webpack-cli babel-loader @babel/core @babel/preset-env
$ yarn webpack

できたdistパッケージをaws_chime_django_local/src/video_meeting_app/static/以下に配置します。

動作確認

2つの違うデバイスでそれぞれ会議に参加してみたいと思います。
今回はngrokを使ってトンネルします。

$ ngrok http 8000

そして、でてきたurlでアクセスします。
※ ここで新たなドメインからDjangoにはアクセスできないので、プロジェクトのsettings.pyに以下を追記します。

# aws_chime_django_local/src/aws_chime_django/settings.py
ALLOWED_HOSTS = ["0.0.0.0", "localhost", "127.0.0.1", "your/ngrok/tunnel/url"]

そうすると以下のような簡易的な画面が出てくるので、適当に入力して会議を作成し、参加ボタンを押します。

参加ボタンを押すと会議の画面に行きます。
参加を押すとカメラやオーディオへのアクセス権限の確認のポップアップが出るので許可をします。
これをそれぞれのデバイスで行い、それぞれの画面が共有できていること、音声が聞こえていることを確認できます。

※ ローカルでの開発の場合、ブラウザにもよりますが0.0.0.0だとカメラアクセスがうまくいかない時があるので、そのときはlocalhostであればうまくいくことが多いです。

最後に

Amazon Chimeを使ってWeb会議をできるようにしました。実際に使うにはまだまだ修正する必要がありますが、勉強になりました。

参考

Discussion