📹

SkyWay SFUで実現!1Room×複数カメラの“中央監視システム”構築

に公開

はじめに

今回は、SkyWayを利用した「中央監視システム」のような実装例を紹介していきます。

そもそも SkyWayって何? と気になった方もいるかもしれません。公式サイト[1]では、次のように説明されています。

SkyWayは、ビデオ・音声・データ通信機能をアプリケーションに簡単に実装できる
SDK&APIです。ビデオ会議や、オンライン診療、遠隔ロボットなど、リアルタイムな
コミュニケーションを実現します。

要するに、SkyWayは、「テレビ電話・音声通話・チャット機能」を簡単にくっつけられるパーツセットです。

しかし、今回で紹介したい実装は、一般的なビデオ通話とはちょっと違います。

異なる点はこちらです。

  • カメラ側とビューワー側を分ける
  • カメラ側は配信のみ行う。ビューワーの動画や音声を受け取らない。
  • ビューワー側は対象ルームに参加して動画や音声を表示するのみ。配信に参加しない。

つまり、「監視カメラ↔︎セキュリティ室の中央監視システム」を想像してもらえると分かりやすいかと思います。

システム概要

  1. カメラ側(配信)

    • デバイスのビデオカメラから SkyWay SFU Room へ配信
    • 同一Roomに複数配信可能
  2. ビューワー側(受信)

    • Roomに参加し、全配信をSubscribe
  • SkyWay SFU Room を使って複数カメラが同時に動画/音声を配信
  • 複数のビューワーが同時にそれらを受信可能
  • ルームは共通。全カメラは同一ルームに配信

用語の確認

この後、実装に出てくるいくつかの用語を簡単に紹介します。

Room

SkyWayのパーツセットの中にRoomが存在します。
Roomについて、ユーザーガイド[2]では以下のように説明されています。

通話を行うグループの単位であり、ユーザーは共通の Room に参加したユーザー同士で通話を行います。 Room に参加しているユーザーのことを Member と呼びます

P2P Room と SFU Room の 2 種類の Room があります

  • P2P Room は少人数向け
    上限人数に制限はありませんが、ユーザーが快適に通話できる人数は 4 人までです
  • SFU Room は多人数向け
    SFU サーバーを経由してメディア通信を行います

今回はSFU Roomを利用します。

Stream(ストリーム)

送受信するデータ

  • AudioStream:マイク音声など
  • VideoStream:カメラ映像など
  • DataStream:任意のデータ(※SFUでは使えない)

Publish(公開)

カメラ側で使用する

StreamをRoomに公開すること
Publicationというリソースが作られる

Subscribe(購読)

ビューワー側で使用する

Publicationを受信すること
Subscriptionを通じて、映像や音声を取得できる

実装例

配信側(カメラ側)

SkyWayCamera.vue

<template>
  <div>
    <p>ID: <span>{{ myId }}</span></p>
    <div>
      channel name:
      <input v-model="channelName" type="text" />
      <button @click="join">join</button>
    </div>
    <video ref="localVideo" width="400px" muted playsinline></video>
  </div>
</template>
import  GetToken  from '~/plugins/SkywayToken.js'

export default {
  data() {
    return {
      myId: '',
      channelName: '',
      audio: null,
      video: null,
      me: null,
    };
  },
  mounted() {
    this.setupLocalStream();
  },
  methods: {
    async setupLocalStream() {
      const { SkyWayStreamFactory } = await import('@skyway-sdk/room');
      const { audio, video } = await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream();
      this.audio = audio;
      this.video = video;

      video.attach(this.$refs.localVideo);
      await this.$refs.localVideo.play();
    },
    async join() {
      if (!this.channelName) return;

      const {
        nowInSec,
        SkyWayAuthToken,
        SkyWayContext,
        SkyWayRoom,
        uuidV4,
      } = await import('@skyway-sdk/room');

      const token = GetToken(process.env.SKY_WAY_APP_ID, process.env.SKY_WAY_API_SECRET);
      const context = await SkyWayContext.Create(token);
      const channel = await SkyWayRoom.FindOrCreate(context, {
        type: 'sfu',
        name: this.channelName,
      });

      this.me = await channel.join();
      this.myId = this.me.id;

      await this.me.publish(this.audio);
      await this.me.publish(this.video, {
        encodings: [
          { maxBitrate: 80_000, id: 'low' },
          { maxBitrate: 400_000, id: 'high' },
        ],
      });
    },
  },
};

受信側(ビューワー)

index.vue

<template>
  <div>
    <p>ID: <span>{{ myId }}</span></p>
    <div>
      channel name:
      <input v-model="channelName" type="text" />
      <button @click="join">join</button>
    </div>
    <div ref="remoteMediaArea" class="video-grid"></div>
  </div>
</template>

受信側は同じルームにjoinし、全配信をSubscribeします

import  GetToken  from '~/plugins/SkywayToken.js'

export default {
  data() {
    return {
      myId: '',
      channelName: '',
      me: null,
    };
  },
  methods: {
    async join() {
      if (!this.channelName) return;

      const {
        nowInSec,
        SkyWayAuthToken,
        SkyWayContext,
        SkyWayRoom,
        uuidV4,
      } = await import('@skyway-sdk/room');

      const token = GetToken(process.env.SKY_WAY_APP_ID, process.env.SKY_WAY_API_SECRET);
      const context = await SkyWayContext.Create(token);
      const channel = await SkyWayRoom.FindOrCreate(context, {
        type: 'sfu',
        name: this.channelName,
      });

      this.me = await channel.join();
      this.myId = this.me.id;

      const subscribeAndAttach = async (publication) => {
        if (publication.publisher.id === this.me.id) return;

        const { stream, subscription } = await this.me.subscribe(publication.id);
        switch (stream.contentType) {
          case 'video': {
            const videoElm = document.createElement('video');
            videoElm.playsInline = true;
            videoElm.autoplay = true;
            videoElm.width = 320;
            stream.attach(videoElm);

            videoElm.onclick = () => {
              const next = subscription.preferredEncoding === 'low' ? 'high' : 'low';
              subscription.changePreferredEncoding(next);
            };

            this.$refs.remoteMediaArea.appendChild(videoElm);
            break;
          }
          case 'audio': {
            const audioElm = document.createElement('audio');
            audioElm.controls = true;
            audioElm.autoplay = true;
            stream.attach(audioElm);
            this.$refs.remoteMediaArea.appendChild(audioElm);
            break;
          }
        }
      };

      channel.publications.forEach(subscribeAndAttach);
      channel.onStreamPublished.add((e) => subscribeAndAttach(e.publication));
    },
  },
};

最後に

SkyWay SFUを使うことで、複数のカメラが同時に配信され、それらを複数のビューワーが同時に受信することが可能になりました。

これは、セキュリティ監視や店舗・施設の遠隔モニタリング など、さまざまな用途で応用可能です。

参考リンク

脚注
  1. SkyWay公式サイト SkyWayとは ↩︎

  2. SkyWay JavaScript SDK について ↩︎

ファースト・スクラッチTech Blog

Discussion