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

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

vue + skywayアプリでRealtime Databaseを使う

2020/07/07の日報

Hanasot開発の日報、第11日目です。
やったことは

  • vue + skywayでRealtime Databaseを使う

です。
時間は、

  • Hanasot開発 1h26min
  • 日報ブログ 2h52min

です。
運動は

  • アンイーブン・スクワット 左右17repsずつ * 2

です。

vue + skywayでRealtime Databaseを使う

今日は「発言者を1人に限定してみよう(発言者以外はミュートしよう)」のpart1です。そもそもこの機能はHanasotの骨組みである哲学対話のルールに基づいています。哲学対話で重要とされるルールの1つに、「発言中に口を挟ませないために、対話中に発言できるはマイクを持っている人のみ」というものがあるんです。他人との対話を通して自分の心と対話する哲学対話の思想に深く結びつくルールといえるでしょう。

今日から2,3日かけてこの機能を作っていきます。

実装方法

この機能はDBをリアルタイムで反映させることで実現します。具体的には下記のフローです。

1.skywayでルームが作成されたら、同時にfirebaseでもルーム一覧コレクションにドキュメントを作成

2.ドキュメントは入れ子にして

【ルーム一覧コレクションのドキュメント】
・ルームID
・現在の発言者(初期値はルームの作成者。リアルタイムで更新する。)
・作成日時
・参加者一覧(サブコレクション)

【参加者一覧コレクションのドキュメント】
・参加者ID
・マイクはオンかオフか(リアルタイムで更新する。)
・カメラはオンかオフか(リアルタイムで更新する。)
・参加日時

このように保存する。参加者一覧のドキュメントは参加者が入室する度に追加されていく。
(文字だとわかりにくいかと思いますので、下の動作時の画像も合わせてご確認ください。)

3.DBを常に監視して、発言者以外はマイクをオンにできないようにする(発言者がマイクをオフにするのは可能)

4.発言者が発言権を譲ったらDBの発言者を更新して、マイクのオンオフを切り替える

参加者ごとに保存しているカメラのオンオフ・マイクのオンオフのデータは、明日以降UIの部分で使う予定です。

firebaseにコレクションを作成する

firebaseコンソールのdatabaseタブを開き「コレクションを開始」をクリックして作成していきます。

f:id:chusotsuengineer:20200707223842p:plain
コレクション名を入力
f:id:chusotsuengineer:20200707224146p:plain
最初のドキュメントは使用しませんが、DB構成を明確にするために入力しておきます
f:id:chusotsuengineer:20200707224424p:plain
サブコレクションも作っていきます
f:id:chusotsuengineer:20200707224453p:plain
サブコレクションの構成はこんな感じです

これでコレクションの作成と、DBに書き込む項目がわかりました。早速コードを書きましょう。

~~~~略~~~~
  import firebase from 'firebase';
~~~~略~~~~
    methods: {
      makeRoom() {
        const mediaConnection = this.peer.joinRoom(this.roomID, {
          mode: 'sfu',
          stream: this.localStream,
        });

        // ルームを作成
        firebase.firestore().collection('rooms').doc(this.roomID).set({
          createdAt: firebase.firestore.FieldValue.serverTimestamp(),
          right: this.peer.id,
        });
        firebase.firestore().collection('rooms').doc(this.roomID).collection('participants')
          .doc(this.peer.id).set({
            isMuted: false,
            isCameraOff: false,
            joinedAt: firebase.firestore.FieldValue.serverTimestamp(),
          });

        this.setEventListener(mediaConnection);
      },
~~~~略~~~~

firebase.firestore().collection('コレクション名').doc('ドキュメントのID').set(書き込む項目)というように記述します。

サブコレクションがあるならfirebase.firestore().collection('コレクション名').doc('ドキュメントのID').collection('コレクション名').doc('ドキュメントのID').set(書き込む項目)という感じで2回コレクションを指定します。直感的でいいですね。

DBと接続する

ただこのままだとエラーが起きてしまいます。必要な環境変数を設定していないからです。vueのコンストラクタを呼んでいるjsファイルを開きましょう。vue-cliならindex.jsだと思います。その先頭に下のコードを書きます。

import firebase from 'firebase';

const config = {
  apiKey: 'apiキー',
  authDomain: 'ホスティング先のURL',
  databaseURL: 'DBのURL',
  projectId: 'プロジェクト名',
  storageBucket: 'ストレージ(画像とかを保存する)のURL',
  messagingSenderId: 'よくわからん',
}

firebase.initializeApp(config);

configの取得方法はドキュメントに書いてあります。が、既にホスティングしているならこんなことしなくても大丈夫です。
ホスティング先のURL/__/firebase/init.jsにアクセスすればjson形式で表示されます。マジで。セキュリティどうなってんのという気もしますが今回はスルー。

ルールを書き換える

これで万事OKと思いきやこんなエラーが出ました。

f:id:chusotsuengineer:20200707230202p:plain
真っ赤やん
前のプロジェクトで1億回は見たエラーなのでもはや慌てません。これはセキュリティルールに引っかかった時に吐くエラーです。

f:id:chusotsuengineer:20200707230310p:plain
コンソールでルールを確認してみる

前回チラ見したとき、このルールは「どのドキュメントにもアクセスOK」の意味だと思ってましたが、よくみるとif falseになってますね。どのドキュメントもアクセスさせねーよってことでした。ここtrueに書き換えるだけでとりあえずOKです。公開前にちゃんと制定していきます。

コレクション作成完了

あとはこんな感じでルームIDを入力して送信してみると

f:id:chusotsuengineer:20200707230608p:plain
謎の後光が差す猫

f:id:chusotsuengineer:20200707230715p:plain
日本語のidでもOKなのは知らなかった
f:id:chusotsuengineer:20200707230817p:plain
サブコレクションもちゃんとできてます

成功ですね。

所感

参加者一覧のとこサブコレクションにしようかmapにしようか迷ったんですけど、Timestampがmapでは使えなかったのでサブコレクションにしました。そんな違いがあるんですね。

ルールはガチガチに固めないとヤバそうなので早め早めに作っていきます。
あと、ルールをローカルに反映するのも忘れないようにしないとdeployで消えちゃうので気をつけましょう。 そんなところかな。

なんか今日やたら記事を書くのに時間かかってグロッキーです。なんでかなー。

vue + skywayでミュート・カメラオフ機能を実装する

2020/07/06の日報

Hanasot開発の日報、第10日目です。
やったことは

  • vue + skywayでミュート・カメラオフ機能を実装する

です。
時間は、

  • Hanasot開発 1h26min
  • 日報ブログ 1h

です。
運動は

  • レバー・プッシュアップ 左右17repsずつ→左右12repsずつ→左右13repsずつ

です。

vue + skywayでミュート・カメラオフ機能を実装する

ちょっと時間かかりましたが割とサクッとできました。読み込み時は下の画像のように

  • カメラオン
  • マイクオフ

の状態です。

f:id:chusotsuengineer:20200706220753p:plain
カメラオン・マイクオフ(ちなみにトトロの方はiPadです)

このマイクボタンとカメラボタンをクリックすることによってオンオフが切り替わります。

f:id:chusotsuengineer:20200706220852p:plain
マイクオン・カメラオフ(相手の方でも黒い画面になっています)
f:id:chusotsuengineer:20200706220943p:plain
マイクオン・カメラオン

で、今回追加したソースコードはこちらです。

<template>
  <div>
    <div class="c-video">
      <video id="my-video" width="400px" autoplay muted playsinline></video>
      <div class="c-video__mic" @click="isMuted = !isMuted">
        <img :src="require(isMuted ? '../assets/mic_off.svg' : '../assets/mic.svg')">
      </div>
      <div
        class="c-video__camera"
        @click="isCameraOff = !isCameraOff">
        <img :src="require(isCameraOff ? '../assets/camera_off.svg' : '../assets/camera.svg')">
      </div>
    </div>
~~~~中略~~~~
    data() {
      return {
        localStream: null,
        roomID: '',
        peer: {},
        participants: [],
        isMuted: true,
        isCameraOff: false,
      }
    },
    watch: {
      isCameraOff(newValue) {
        const stream = this.localStream
        stream.getVideoTracks()[0].enabled = !newValue;
        this.localStream = stream;
      },
      isMuted(newValue) {
        const stream = this.localStream
        stream.getAudioTracks()[0].enabled = !newValue;
        this.localStream = stream;
      },
    },
    mounted() {
      // カメラ映像取得
      navigator.mediaDevices.getUserMedia({video: true, audio: true})
        .then( stream => {
          stream.getAudioTracks()[0].enabled = !this.isMuted;
          stream.getVideoTracks()[0].enabled = !this.isCameraOff;
          // 成功時にvideo要素にカメラ映像をセットし、再生
          const videoElm = document.getElementById('my-video')
          videoElm.srcObject = stream;
          videoElm.play();
          // 着信時に相手にカメラ映像を返せるように、グローバル変数に保存しておく
          this.localStream = stream;
~~~~中略~~~~

順番に解説していきます。

マイク・カメラボタンがクリックされたら画像を切り替える

この部分ですね。

<div class="c-video__mic" @click="isMuted = !isMuted">
  <img :src="require(isMuted ? '../assets/mic_off.svg' : '../assets/mic.svg')">
</div>

これはマイクですが、カメラもほぼ一緒です。 重要なのは@click="isMuted = !isMuted":src="require(isMuted ? '../assets/mic_off.svg' : '../assets/mic.svg')"になります。前者がv-on、後者はv-bindと呼ばれる機能です。

クリックなどのイベントを検知するv-on

v-on:click=""を省略して書くと@click=""となります。コード量も少なくわかりやすいので基本的にこちらを使うことがほとんどです。今回は@click="isMuted = !isMuted"と記述しています。これは「ボタンがクリックされたときに、dataプロパティで定義しているisMuted = true```を trueならfalse に、 falseならtrue に反転させてください」という意味です。

htmlの属性にjsの処理を書けるv-bind

そしてその下のimgタグのsrc属性には:src="require(isMuted ? '../assets/mic_off.svg' : '../assets/mic.svg')"とあります。これは「isMutedがtrueならマイクの画像を、falseならマイクに斜線の入った画像を表示してください」という意味ですね。v-bindの時はレンダリングの関係?でrequireでパスを指定しないと適切なパスが出力されないので注意。あ、v-bind:src=""の省略は:src=""で、src以外にも:class=""とかもできます。属性ならなんでもOKです。

これらを使って、ボタンの表示を切り替えてます。ここまではUIの処理です。

isMutedとisCameraOffを監視して、カメラとマイクの使用許可を切り替える

次に本題のボタンの真偽値を監視して、カメラとマイクのオンオフ切り替えを行う処理ですね。watchプロパティを使います。

    watch: {
      isCameraOff(newValue) {
        const stream = this.localStream
        stream.getVideoTracks()[0].enabled = !newValue;
        this.localStream = stream;
      },
      isMuted(newValue) {
        const stream = this.localStream
        stream.getAudioTracks()[0].enabled = !newValue;
        this.localStream = stream;
      },
    },

これなにをしているかというと、「関数名と同名のdataプロパティを監視して、値が切り替わったら関数を実行する」というものです。今回は「isMutedを監視して、真偽値が切り替わったら自分の映像を格納しているlocalStreamを取ってきて、マイクの許可をオンオフする」という処理をしてます。カメラも同じですね。

getAudioTracks()[0].enabledという部分はデバイス使用の許可を表します。trueならマイクの使用可能、falseならマイクの使用不可です。これはskywayの機能ではなく、ブラウザのデフォルトの機能になります。映像や音声の送信を止めたりするというよりは、カメラやマイクが使えなくなって結果的にミュートになったりカメラオフになったりするというわけですね。

ちなみにmountedで

          stream.getAudioTracks()[0].enabled = !this.isMuted;
          stream.getVideoTracks()[0].enabled = !this.isCameraOff;

このようにenabledを定義しています。これをしなくてもオンオフ切り替えはできるような気がするんですが、初期値が不許可の場合に限り上手く動きませんでした。ですので、dataプロパティの値を画面読み込み時に一応定義しておくのが無難かと思います。

所感

結構進みましたねー。いよいよそれらしくなってきた感じがします!まだまだ先は長いですが、「お題を出す機能」と「発言者を一人に限定する機能」さえ作ればアイスブレイクくらいはすぐ実装できそうです。がんばります!

vueとskywayによるビデオ通話で参加者離脱の処理をする

2020/07/05の日報

Hanasot開発の日報、第9日目です。
やったことは

  • vueプロジェクトのskywayによるグループ通話で参加者が離脱した際の処理を追加する

です。
時間は、

  • Hanasot開発 31min
  • 日報ブログ 37min
  • 日報ブログ引っ越し準備 59min

です。
運動は

  • アンイーブン・プルアップ 左右7repsずつ * 2(右手の1セット目は初めてスタローン流でできました!)
  • フル・プルアップ 順手 8reps
  • フル・プルアップ 逆手 8reps

です。

skywayによるグループ通話で参加者が離脱した際の処理を追加する

前回の記事でskywayによるグループ通話は開通しました。これで任意のルームを作成して、複数人によるビデオ通話自体はできることになります。加えてHanasot(哲学対話web)で必要な機能は

  • ミュート・カメラオフ機能
  • 発言権を1人に限定する機能(横から口を挟ませない)
  • 発言権のリクエストをする機能(挙手マークを画面上に出すなど)
  • 発言権を他の参加者に渡す機能(基本的に挙手している人に渡す)

という感じですね。で、今日実装したのは「もし参加者が離脱した場合に、離脱した参加者の映像を消す」機能です。哲学対話では対話中の離脱は基本的に想定されないのですが、通信状態の悪化やPCの電源オフ、ブラウザを閉じるなどの動作によって離脱してしまった場合に、残った参加者で対話を続けるための機能です。それではソースコードを見ていきましょう。

      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();
        });
      },
      async removeVideo(mediaConnection) {
        const participants = [];
        let remoteStreams = [];
        // 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);
        });
        mediaConnection.on('peerLeave', function () {
          self.removeVideo(mediaConnection);
        });
      }
    },

今日追加したのはこの部分です。順番に解説していきます。

まず'peerLeave'イベントを捕まえるように、setEventListenerで登録しておきます。これは参加者が離脱した際に発火するイベントです。
発火したらself.removeVideo(mediaConnection);メソッドを呼びます。中身はaddVideoとほとんど同じものです。なぜかというと

    <video
      v-for="participant in participants"
      :key="participant"
      :id="participant"
      width="400px"
      autoplay muted playsinline></video>

このように参加者の映像を出力するvideoタグをv-forで生成しているためです。参加者が減ろうが増えようがdataプロパティに登録されているparticipantsの値さえ正しければ、参加者の人数分のvideoタグを生成してくれます。addVideoとちょっと違うのは下記の部分ですね。

// removeVideoにはこのイベントを設定していません
mediaConnection.on('stream', () => {

このstreamは相手方の映像が届いたときに発火するイベントです。今回は参加者が離脱する際に発火するpeerLeaveイベントから始まるため、相手方の映像は既に表示されています。したがってstreamイベントが発火しないんです。addVideoと同じ処理でいけたら記述量が少なくなったんですが、仕方ありませんね。

今回のハマりどころと解決方法

いうほどハマってないんですが、実装するまではaddVideoでremoveVideoの処理もまかなえるだろうと思っていました。理由は前述したとおりv-forを使っているのでparticipantsの中身を適切なものに変えるだけで、videoタグの増減は行われるからです。でも単純にpeerLeaveイベントでaddVideoを呼び出しても想定通りの動作はしませんでした。そこでこんなふうにして原因を調べました。

f:id:chusotsuengineer:20200705223101p:plain
setEventListener内のconsole.log
f:id:chusotsuengineer:20200705223138p:plain
addVideo内のconsole.log
f:id:chusotsuengineer:20200705223204p:plain
出力結果。streamイベントに絡めた処理は行われていないことがわかる。

実行されるであろう箇所にひたすらconsole.logを入れるんです。単純で最強のデバッグ。jsの好きなところを挙げてといわれたら「ログの出しやすさ」は間違いなく挙げると思います。めっちゃ簡単なんですもん。

ということで、こんなふうにしてstreamイベントは参加者が離脱した時には発火しないことを見つけたら後はサクサクとできました。

所感

ログ大事だなーってのは仕事でも常々思います。期待通りの動作をしなかったら面倒でもすぐにログをいっぱい仕込んで確認した方が絶対早いです。ソースコードとにらめっこする前にデータはどこでどうなっているのかをちゃんと把握していないと意味がないですからね。

明日はミュートとカメラオフ機能の実装をやっていきます。がんばります。

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タグを削除する機能

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

ちょっと早く旅に出る猫の話

2020/07/03の日報

Hanasot開発の日報、第7日目です。
今日は昨日に引き続き

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

を行いました。
時間としては、

  • Hanasot開発 1h8min
  • 日報ブログ 1hくらい(測り忘れた)

です。
運動は

です。

ちょっと早く旅に出る猫の話

今日はあんまり進捗もないし、ちょっとだけうちの猫の話を書きます。
はじめて家に来たのは一昨年の夏。走れるようになったばかりくらいの子猫で、人を見ると一目散に逃げ出すような警戒心の強い子でした。カリカリをお皿にいれて僕が家に入るとそろそろと食べて、食べ終わるとどこかに行く。サビにも見えるようなキジトラと茶トラの間くらいの柄で、鍵尻尾で折れ目にちょうど白いラインが入っていて、それでとても綺麗な顔をした女の子でした。

1ヶ月くらいかけて徐々に慣れていって、半年後には家の中で飼えるようになって、どこからか弟猫も連れてきて、避妊手術をして、それからしばらくしたら脱走して。いろんなことがありましたが今では幸せそのもののような存在でした。鳴き声は鈴を転がすような声で短く「ニャッ」って鳴くんです。でもおしゃべりで色んな鳴き声を聞かせてくれました。そのどれもがかわいくて、本当に好きだった。普段は触られるのが好きじゃないけど、椅子やソファーや布団で寝ているときに撫でると心地よさそうにゴロゴロと喉を鳴らします。喉を撫でると撫でやすいように上を向いてくれて、そのときに見える白い喉の毛がふわふわしていてこの子のチャームポイントでした。機嫌がいいときはお腹をなでている僕の手を舐めてくれたりもしたなあ。

好きな食べ物はちゅーると鰹節で、食べたいときは足にすりっと体をすりつけておねだりします。短いけどふわふわの毛とちょっと太り気味のお腹がとてもさわり心地よくて、かわいい顔で粘り強く待ち続けるからついついおやつをあげてしまって。またおねだりしてほしいよ。ちゅーるでも鰹節でもカリカリでも何でもあげるのに。

弟猫は白黒で見た目は全然似ていませんが、本当に仲良しでいつも一緒にいました。じゃれたり毛づくろいしてあげたり、抱き合って寝たりして、それを見ていると凄く幸せな気持ちになりました。弟猫はわかっていないみたいだけど、夢で会えたりするのかなあ。そうだといいな。

今日はもう遅いので明日ペット霊園で火葬してもらいます。ごはんとお花と写真を持ってきてくださいと言われたのでカリカリと鰹節と4本パックのちゅーるを買ってきてたんですけど、天国へ旅に出るならきっと4本じゃ足りないよなと思ってお徳用の20本パックのちゅーるを買い足しました。これなら足りるよね。まぐろのちゅーるとささみバラエティだからきっと満足してくれるはずです。いつものカリカリと鰹節も残さず食べてくれるでしょう。お花はよく遊んでいたうちの庭のお花を摘んであげようと思います。

じゃあね小梅ちゃん。気をつけて。またいつかね。

f:id:chusotsuengineer:20200703213926j:plain
左が小梅で右が弟のシロ

vueプロジェクトでskywayを使ったグループ通話ができなかったので超力技で半分解決した話

2020/07/02の日報

Hanasot開発の日報、第6日目です。
今日は昨日に引き続き

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

を行いました。
時間としては、

  • Hanasot開発 1h20min
  • 日報ブログ 1h11min

です。
運動は

  • 包丁研ぎ 3本(地味にきついので入れさせてください)
  • エアロバイク 15min

です。

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

さて、昨日の記事でハマっているとお伝えしたグループ通話ですが、ちょっと進展しました。

f:id:chusotsuengineer:20200702215441p:plain
クレーンゲームで取った猫×3
参加者の数だけvideoタグを生成することに成功したのです。素晴らしい。
次にソースコードをご覧ください。

<template>
  <div>
    ~~~中略~~~
    <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 {
    ~~~中略~~~
        participants: [],
      }
    },
    methods: {
      makeCall() {
        const mediaConnection = this.peer.joinRoom(this.theirID, {
          mode: 'sfu',
          stream: this.localStream,
        });
        this.setEventListener(mediaConnection);
      },
      async setEventListener(mediaConnection) {
        const participants = [];
        let remoteStreams = [];
        Promise.all(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.map(remoteStream => {
          const videoElm = document.getElementById(remoteStream.peerId);
          videoElm.srcObject = remoteStream;
          videoElm.play();
        });
      }
    },
  }
</script>

おわかりでしょうか。昨日thisの対象が変わってしまってparticipantsがundefinedになってしまっていたのをループ外に出すことで対応し、DOMが生成されるまで1秒待機してvideoタグにsrcを設定しています。
うん。ダサい。クソダサいです。
まあそれは一旦置いておきましょう。リファクタリングは後からできますからね。きっとしますよ。大丈夫。がんばれ未来の僕。
問題はこちらです。

f:id:chusotsuengineer:20200702220128p:plain
2番目に通話に参加した人の画面
f:id:chusotsuengineer:20200702220210p:plain
最初に通話に参加した人の画面
違いわかりますかね。難しいですがよーく見てください。2番目に参加した人の画面には3番目に参加した人が映っておらず、最初に参加した人の画面には2番目の人も3番目の人も映っていません。
なぜだ...

DOMがどうもダメっぽい

表示されていない1番目の人のDOMを調べてみるとですね。videoタグ自体は生成できているんです。idもちゃんと相手方のものが設定された状態で。
その上で映像だけが映っていないとなれば問題はsrcの設定ということになります。やっぱawaitで1秒待たせて無理やり映してるのがだめなのかなぁ。

所感

ちょっと今日は時間がなくて軽くしかまとめられませんでしたが、今はこんな状況です。なかなか複数人通話ができませんね。うーむ。
明日はsrcタグの設定にあたりをつけてやっていきます。先輩に助言を求めてみようかな。

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

2020/07/01の日報

Hanasot開発の日報、第5日目です。
今日は

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

を行いました。
時間としては、

  • Hanasot開発 1h3min
  • 日報ブログ引っ越し準備 1h44min
  • 日報ブログ 1h11min

です。
運動は

*ハーフ・ハンドスタンド・プッシュアップ 20reps → 20reps → 16reps

です。

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

実はこれまだ終わっていません。昨日までの日報ではサクサクと進んで進捗らしい進捗を報告できていましたが、流石にそう毎日うまくはいきませんね。
ですので今日は、 「何をしようとしていて」⇒やりたいこと 「何でつまづいているのか」⇒ハマっている箇所 の報告です。

やりたいこと
必要なこと

昨日の時点でskywayを用いてビデオ通話を行うことはできていました。それから発展して今回は複数人によるグループ通話を実装しようと思っています。また、skywayに対してグループ通話を行いたいという旨のリクエストを送信して、グループ通話用のデータを返してもらうことはできていますから、必要なのは参加者分の動画を表示する機能ということになります。
vueはそういう要素を増やしたり消したりといった処理はとても得意なので、簡単にできるはずです。はずなんです。

動作フロー

動作フローはこんな感じです。

  1. 任意のチャットルームのIDを参加者Aが入力してskywayに送信する
    2.参加者BがAの決めたIDを入力してskywayに送信する
    3.IDがマッチするとskywayがオブジェクトを返すので、それを受け取る
    4.オブジェクトには自分以外の参加者のカメラを通した動画が格納されている(リアルタイムで更新される)
    5.その動画をhtmlのvideoタグのsrcに設定する
    6.ビデオ通話できるので楽しい
    7.参加者CがAの決めたIDを入力してskywayに送信する
    8.skywayがCを含めたオブジェクトを返すので、それを受け取る
    9.C用のvideoタグを追加する
    10.3人で通話できて楽しい
ハマっている箇所

動作フローでいうと5です。3人目の追加ができないよーっていうんじゃなくて2人目からハマってます。
もっというと2人だけでいいならできているんです。ただ3人目を見据えて、「参加者人数に合わせてvideoタグを生成し、それぞれにsrcを設定する」実装ができていません。

詳しくご説明していきます。まずはこういうvideoタグを作りました。

<video
      v-for="participant in participants"
      :key="participant.id"
      :id="participant.id"
      width="400px"
      autoplay muted playsinline></video>

v-forとkey属性はvueで使うもので、「participants配列の数だけvideoタグを作ってください。それとparticipantsの中身をparticipantとしてループ処理してください。生成したタグを見分けるためにparticipantのidを振ってください。」という意味になります。
これは多分問題ないです。

次にこんなふうにdataプロパティでparticipants配列を用意しました。

data() {
  return {
~~~~略~~~~
        participants: [],
      }

上で解説したtemplateで使うためですね。この配列に参加者idを詰めていけば、参加者の数だけvideoタグが生成されて、それぞれのvideoタグにidが振られるはずです。これも問題ないでしょう。

問題はこの処理です。

setEventListener: (mediaConnection) => {
  mediaConnection.on('stream', stream => {
   // video要素にカメラ映像をセットして再生
   Object.keys(mediaConnection.remoteStreams).forEach(async key => {
      const remoteStream = mediaConnection.remoteStreams[key];
      const remoteId = remoteStream.peerId;
      await this.participants.push(remoteId);
      const videoElm = document.getElementById(remoteId);
      videoElm.srcObject = remoteStream;
      videoElm.play();
    })
  });
}

ややこしいですが順に解説していきます。
あ、今回は省略していますがこのsetEventListenerという関数は着信を検知して呼び出されています。誰かがルームに入ったぞ、となったら呼び出されるわけですね。
まずは

 mediaConnection.on('stream', stream => {

の部分です。これは参加者の動画を受信した時に発火します。参加者が入室したことによりsetEventListenerが呼ばれて、その後動画もちゃんと送られてきたらこの中の処理に入っていくということです。

次にちょっとややこしい

Object.keys(mediaConnection.remoteStreams).forEach(async key => {

の部分です。
これは参加者情報(remoteStreams)を一人分ずつ処理するためにループで回しています。mapやforEachで書けばスッキリなのですが、skywayから返ってくるデータがオブジェクト型のため「keyを配列の形で取り出して、取り出したkey配列をforEachで回す」という処理を行わなければならないので、こんなややこしくなっちゃってるんですね。

そして

const remoteStream = mediaConnection.remoteStreams[key];
const remoteId = remoteStream.peerId;
await this.participants.push(remoteId);

これですね。上で解説したようにオブジェクトのkey配列をforEachで処理しているので、オブジェクト[key]とすることで目的の「参加者1人分のデータ」を取り出して定数に詰めています。
remoteStream.peerIdと記述することで参加者固有のIDを取り出します。それをvideoタグ生成のためのparticipantsに追加するわけです。したいんです。でもできないんです。ここが問題なので videoElm以下は割愛します。

arrow関数と普通の関数

本当の問題はmediaConnection.on('stream', stream => {にあります。これarrow関数っていうんですけど、ES6から実装された構文です。メリットはfunctionsって書くより短く書けることです。絶対それだけじゃないんですけど僕レベルではそれだけです。ただこれvueで扱うとき問題があって「宣言元のthisを参照する」んですよ。端的に言うとthis.hogehogeとしてもdataプロパティを参照できなくなります。
そう、つまり先程this.participants.pushとしていた箇所で、dataプロパティのparticipantsにアクセスできないんですね。うん。arrow関数やめればいいじゃん。試したっけ。試してないよ多分。

こんなしょーもないことでハマってたのか。明日ちゃんと普通の関数で試してみます。

所感

自分のやってることを整理するの大事

この記事を書く直前までarrow関数を使わねばならないものだと決めつけていました。実際普通にfunctionsと書けばそれで解決する問題なのに。
うーん。仕事でもこういうのやらかしてそうだなぁ。なんか「こうしなきゃダメだ!」って勝手に縛りを作っていて、その縛りを回避しようと無駄なコードを書くことある気がするんですよ。悩んだらちょっと頭を冷やしてやりたいことを整理するのが大事ですね。