ある中卒がWeb系エンジニアになるまでと、それからのこと

うつ病で高校を中退したり、たこ焼き屋のオヤジにホームページとたこ焼きを作らされたり、そのホームページが訴えられそうになったり、弁護士を目指したりした後にエンジニアになった人が書くブログ

skywayによるグループ通話でハマってた箇所がアホすぎた話

2020/07/04の日報

Hanasot開発の日報、第8日目です。
今日はお葬式と飲み会だったので遅くなりました。
やったことは昨日に引き続き

  • vueプロジェクト内でskywayによるグループ通話を実装する

です。
時間としては、

  • Hanasot開発 24min
  • 日報ブログ 1h1min

です。
運動は

  • ウォール・ウォーキング・ブリッジ 8reps * 2

です。

vueプロジェクト内でskywayによるグループ通話を実装する

ついにskywayによるグループ通話が成功しました。ソースコードはこちらです。

<template>
  <div>
    <video id="my-video" width="400px" autoplay muted playsinline></video>
    <p id="my-id"></p>
    <textarea id="their-id" v-model="roomID"></textarea>
    <button id="make-call" @click="makeCall">発信</button>
    <video
      v-for="participant in participants"
      :key="participant"
      :id="participant"
      width="400px"
      autoplay muted playsinline></video>
  </div>
</template>
<script>
  import Peer from 'skyway-js';
  export default {
    name: 'Dialogue',
    data() {
      return {
        localStream: null,
        roomID: '',
        peer: {},
        participants: [],
      }
    },
    mounted() {
      // カメラ映像取得
      navigator.mediaDevices.getUserMedia({video: true, audio: true})
        .then( stream => {
        // 成功時にvideo要素にカメラ映像をセットし、再生
        const videoElm = document.getElementById('my-video')
        videoElm.srcObject = stream;
        videoElm.play();
        // 着信時に相手にカメラ映像を返せるように、グローバル変数に保存しておく
        this.localStream = stream;
      }).catch( error => {
        // 失敗時にはエラーログを出力
        console.error('mediaDevice.getUserMedia() error:', error);
        return;
      });

      // skyway接続
      this.peer = new Peer({
        key: 'skywayコンソールから発行したid',
        debug: 2,
      });
      this.peer.on('open', () => {
        document.getElementById('my-id').textContent = this.peer.id;
      });

      // エラー時処理
      this.peer.on('error', err => {
        alert(err.message);
      });
    },
    methods: {
      makeCall() {
        const mediaConnection = this.peer.joinRoom(this.roomID, {
          mode: 'sfu',
          stream: this.localStream,
        });
        this.setEventListener(mediaConnection);
      },
      async addVideo(mediaConnection) {
        const participants = [];
        let remoteStreams = [];
        mediaConnection.on('stream', () => {
          // video要素にカメラ映像をセットして再生
          Object.keys(mediaConnection.remoteStreams).forEach( key => {
            const remoteStream = mediaConnection.remoteStreams[key];
            const remoteId = remoteStream.peerId;
            participants.push(remoteId);
            remoteStreams.push(remoteStream);
          });
        });
        this.participants = participants;
        await new Promise(resolve => setTimeout(resolve, 1000));
        remoteStreams.forEach(async remoteStream => {
          const videoElm = document.getElementById(remoteStream.peerId);
          videoElm.srcObject = remoteStream;
          videoElm.play();
        });
      },
      setEventListener(mediaConnection) {
        this.addVideo(mediaConnection);
        const self = this;
        mediaConnection.on('peerJoin', function () {
          self.addVideo(mediaConnection);
        });
      }
    },
  }
</script>
<style scoped>
</style>

で、ハマってたのはここです。

      setEventListener(mediaConnection) {
        this.addVideo(mediaConnection);
        const self = this;
        mediaConnection.on('peerJoin', function () {
          self.addVideo(mediaConnection);
        });
      }

このsetEventListenerは「ルームに新しく参加者が入ってきた時」に発火します。mediaConnection.on('peerJoin',の部分ですね。入室を検知したら映像を追加する処理を走らせるわけです。ここで何にハマってたかというと this の扱いでした。

peerJoinイベントが発火したときに呼び出したいのはmethodsに定義されているaddVideoメソッドです。こいつを呼ぶためには普通はthisを使います。詳しく理解していませんがthisがvueComponentオブジェクトを表すので、thisを使います。今度ちゃんと調べるので今は下記画像で許してください。

f:id:chusotsuengineer:20200705012013p:plain
mountedでthisとwindowをconsoleに出します。通常のjsならどちらもwindowオブジェクトが出力されます。
f:id:chusotsuengineer:20200705012202p:plain
確認してみるとVueComponentオブジェクトが出力されています。

はい。ということでvueで定義したメソッドやプロパティを使うにはthis.hogehogeを使うということですね。で、今回は何が問題だったかというと

        mediaConnection.on('peerJoin', function () {
          // this.addVideo(mediaConnection); と書いていたところを、下記のように変更
          self.addVideo(mediaConnection);
        });

このように.onでイベントに絡めてメソッドを呼び出した場合、thisの表す対象が変わってしまうところですね。this.では関数内しか参照できなくなってしまうのです。これをこうやって解決しました。

      setEventListener(mediaConnection) {
        // thisをselfに入れておく
        const self = this;
        mediaConnection.on('peerJoin', function () {
          // setEventListener内なら参照できるのでselfは呼べる
          self.addVideo(mediaConnection);
        });
      }

超単純です。あの苦労なに?

所感

煮詰まったら一旦リセットしよう

今回学んだことはこれに尽きます。煮詰まってしまったらこんな単純なことすら思いつけません。そしてお客さんの大事なお金を使ってクソコードを書き、この単純な問題を冗長な処理で回避することになります。延々と悩むくらいなら暇を見つけて仲間に聞いたりしたほうがずっといいです。

すぐ人に聞くやつは成長しないけど、ひとりでひたすら煮詰まってるやつ(僕)もだめですね。お互いプラスになるような良い頼り方を覚えていきたいです。

これでグループ通話自体はできたので明日からは

  • ミュート・カメラオフ機能
  • カメラ・マイク選択機能
  • 退出時にvideoタグを削除する機能

あたりをやっていきます。それでは。