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

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

徹夜で9時間作ってるアプリのソースコード

色々あって寝ずにコード書いてました。 現在9時間経過しています。 朝になってしまったのでソースコードだけ公開します。

<template>
  <div style="height: 1000px">
    <header-menu></header-menu>
    <div class="p-dialogue">
      <div class="p-dialogue__content">
        <p class="p-dialogue__txtWrap">
          <span
            class="p-dialogue__txt"
            v-if="!roomID">
            {{`ルームを作成するか、作成済みのルームに参加してください。`}}
          </span>
          <span
            class="p-dialogue__txt"
            v-if="roomID">
            {{`ルームIDは${this.roomID}です。`}}
          </span>
          <span
            class="p-dialogue__txt"
            v-if="!isStarted && roomID && isRight">
            {{`十分な人数が集まったら対話を開始してください。`}}
          </span>
          <span
            class="p-dialogue__txt"
            v-if="!isStarted && roomID && !isRight">
            {{`ホストが対話を開始するまで自由にご歓談ください。`}}
          </span>
          <span
            class="p-dialogue__txt"
            v-if="!roomID && isStarted">
            {{`アイスブレイクタイム。お題は「お気に入りのリラックス方法」です。`}}
          </span>
          <span
            class="p-dialogue__txt"
            v-if="isTalking">
            {{`質問セクションです。お題に関係のないことでも構いませんので、1人1回は質問してみましょう。`}}
            {{`残り時間: ${time}秒`}}
          </span>
        </p>
        <div class="p-dialogue__btnWrap">
          <button
            v-if="!roomID"
            @click="makeRoom"
            class="p-dialogue__btn">ルームを作成</button>
          <button
            v-if="!roomID"
            @click="joinRoom"
            class="p-dialogue__btn">ルームに参加</button>
          <button
            v-if="!isStarted && isRight"
            @click="startDialogue"
            class="p-dialogue__btn">哲学対話を開始する</button>
        </div>
        <div
          class="p-dialogue__videoWrap"
          :class="{'p-dialogue__videoWrap--muted': isMuted}">
          <video
            id="my-video"
            class="p-dialogue__video"
            autoplay muted playsinline>
          </video>
          <div class="p-dialogue__person" :class="{'p-dialogue__person--active': isCameraOff}">
            <img
              src="../assets/person.svg"
              class="p-dialogue__person__img">
          </div>
          <div
            class="p-dialogue__mic"
            :class="{'p-dialogue__mic--disabled': (isStarted && !isRight) && !isTalking}"
            @click="isMuted = !isMuted">
            <img :src="require(isMuted ? '../assets/mic_off.svg' : '../assets/mic.svg')">
          </div>
          <div
            class="p-dialogue__camera"
            @click="isCameraOff = !isCameraOff">
            <img :src="require(isCameraOff ? '../assets/camera_off.svg' : '../assets/camera.svg')">
          </div>
          <div
            class="p-dialogue__timer"
            v-show="time !== 0">
            {{ this.time }}
          </div>
        </div>
        <div
          v-for="participant in participants"
          :key="participant.id"
          class="p-dialogue__videoWrap"
          :class="{'p-dialogue__videoWrap--muted': participant.isMuted}">
          <video
            :id="participant.id"
            autoplay
            playsinline
            @click="transferRight(participant.id)"
            class="p-dialogue__video"></video>
          <div class="p-dialogue__person" :class="{'p-dialogue__person--active': participant.isCameraOff}">
            <img
              src="../assets/person.svg"
              class="p-dialogue__person__img">
          </div>
        </div>
      </div>
    </div>
    <p id="my-id"></p>
  </div>
</template>
<script>
  import firebase from 'firebase';
  import Peer from 'skyway-js';
  import Swal from 'sweetalert2';
  import headerMenu from '@/components/headerMenu.vue';
  import $ from 'jquery';

  export default {
    components: {
      headerMenu
    },
    name: 'Dialogue',
    data() {
      return {
        localStream: null,
        roomID: '',
        joinRoomID: '',
        peer: {},
        participants: [],
        isMuted: true,
        isCameraOff: true,
        isStarted: false,
        isQuestion: false,
        isRight: false,
        isTalking: false,
        right: '',
        time: 0,
      }
    },
    watch: {
      async isCameraOff(newValue) {
        const stream = this.localStream;
        stream.getVideoTracks()[0].enabled = !newValue;
        this.localStream = stream;
        await firebase.firestore().collection('rooms').doc(this.roomID).collection('participants')
          .doc(this.peer.id).update({'isCameraOff': newValue});
      },
      async isMuted(newValue) {
        const stream = this.localStream;
        stream.getAudioTracks()[0].enabled = !newValue;
        this.localStream = stream;
        await firebase.firestore().collection('rooms').doc(this.roomID).collection('participants')
          .doc(this.peer.id).update({'isMuted': newValue});
      },
      async isStarted(newValue) {
        if (!newValue) return;

        if(this.right !== this.peer.id) {
          this.isMuted = true;
          this.isRight = false;
        } else {
          this.isRight = true;
          this.isMuted = true;
          await Swal.fire(
            'トーキングオブジェクトをお渡しします!',
            'アイスブレイクタイムです。\n深呼吸したらマイクをオンにして喋ってみましょう。\n話すことに慣れるのが大事ですよ。',
            'success'
          );
          const self = this;
          await new Promise(resolve => self.timer(resolve, 10));
          await Swal.fire(
            'スピーチお疲れさまでした!',
            '質問セクションに移行します。参加者全員の準備が整うまでお待ち下さい。',
            'success',
          );
          firebase.firestore().collection('rooms').doc(this.roomID).update({
            isQuestion: true,
          });
        }
      },
      async isQuestion(newValue) {
        if (!newValue) return;

        await Swal.fire(
          '質問セクションに移行します!',
          '回答者に質問してみましょう。お題に関係ないことでも構いません。1人1回は質問してみてくださいね。',
          'success'
        );
        await firebase.firestore().collection('rooms').doc(this.roomID).collection('participants')
          .doc(this.peer.id).update({
            isReadyQuestion: true,
          });
      },
      async participants(newValue) {
        if (!this.isStarted) return;
        let isTalking = true;
        await Promise.all(newValue.map(async participant => {
          if (!participant.isReadyQuestion) {
            isTalking = false;
          }
        }));
        this.isTalking = isTalking;
      },
      async isTalking(newValue) {
        const self = this;
        if (!newValue) return;
        await new Promise(resolve => self.timer(resolve, 30));
        await Swal.fire(
          '質問セクション終了です!',
          'お疲れさまでした。\n時間いっぱいになりましたので質問セクションを終了します。\n次の回答者の方の準備が整うまでお待ち下さい。',
          'success',
        );
        await firebase.firestore().collection('rooms').doc(this.roomID)
          .collection('participants').doc(this.peer.id).update({
            isReadyQuestion: false,
          });
        this.isMuted = true;
        this.isTalking = false;
        if (this.isRight) {
          firebase.firestore().collection('rooms').doc(this.roomID).update({
            isQuestion: false,
          });
          const snapshot = await firebase.firestore().collection('rooms')
            .doc(this.roomID).get();
          let respondent = snapshot.data().respondent;

          if (respondent > this.participants.length) {
            Swal.fire(
              'アイスブレイクを終了します!',
              'お疲れさまでした!\n引き続き哲学対話へ移行します。\nしばらくお待ち下さい!'
            )
          }

          console.log('respondentは');
          console.log(respondent);
          console.log('participantsは');
          console.log(this.participants[respondent]);
          await firebase.firestore().collection('rooms').doc(this.roomID)
            .update({right: this.participants[respondent].id});
          await firebase.firestore().collection('rooms').doc(this.roomID)
            .update({respondent: respondent});
        }
      },
      async right(newValue) {
        if (!this.isStarted) return;

        if (newValue !== this.peer.id) {
          this.isMuted = true;
          this.isRight = false;
        } else {
          this.isRight = true;
          await Swal.fire(
            'トーキングオブジェクトを受け取りました!',
            'お題に関係あること、ないこと、どんなことでも構いません。\nあなたの心のままに語ってみましょう。',
            'success'
          );
          const self = this;
          await new Promise(resolve => self.timer(resolve, 30));
          await Swal.fire(
            'スピーチお疲れさまでした!',
            '質問セクションに移行します。参加者全員の準備が整うまでお待ち下さい。',
            'success',
          );
          firebase.firestore().collection('rooms').doc(this.roomID).update({
            isQuestion: true,
          });
        }
      }
    },
    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;
      }).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: {
      timer(resolve, time) {
        this.time = time;
        const self = this;
        const interval = setInterval(function() {
          console.log(self.time);
          self.time--;
          if (!self.time) {
            clearInterval(interval);
            return resolve();
          }
        }, 1000);
      },
      transferRight(participant) {
        if (!this.isRight) return;

        Swal.fire({
          title: 'トーキングオブジェクトを渡しますか?',
          text: "次にトーキングオブジェクトを受け取るまでマイクはミュートされます。",
          icon: 'question',
          showCancelButton: true,
          confirmButtonColor: '#3085d6',
          cancelButtonColor: '#d33',
          confirmButtonText: '渡す',
          cancelButtonText: 'もう少し話す',
        }).then(async result => {
          if (result.value) {
            await firebase.firestore().collection('rooms').doc(this.roomID).update({right: participant});
            Swal.fire(
              'トーキングオブジェクトを渡しました!',
              'みんなの発言を聞くこともまた対話です。\nゆっくりじっくり聞いてみましょう。',
              'success'
            )
          }
        });
      },
      async startDialogue() {
        await firebase.firestore().collection('rooms').doc(this.roomID).update({isStarted: true});
      },
      async makeRoom() {
        this.roomID = Math.random().toString(32).substring(2);

        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(),
          isStarted: false,
          isQuestion: false,
          right: this.peer.id,
          respondent: 0,
        });
        firebase.firestore().collection('rooms').doc(this.roomID).collection('participants')
          .doc(this.peer.id).set({
            isMuted: true,
            isCameraOff: true,
            joinedAt: firebase.firestore.FieldValue.serverTimestamp(),
          });

        this.isRight = true;

        this.setEventListener(mediaConnection);
      },
      async joinRoom() {
        let joinRoomID = '';

        await Swal.fire({
          title: '参加したいルームのIDをご入力ください',
          input: 'text',
          inputAttributes: {
            autocapitalize: 'off'
          },
          showCancelButton: true,
          confirmButtonText: '参加する',
        }).then((result) => {
          joinRoomID = result.value;
        });

        if (!joinRoomID) {
          Swal.fire(
            'ルームIDが未入力です。',
            'お手数ですがもう一度ご入力ください。',
            'warning'
          );
          return;
        }

        const snapshot = await firebase.firestore().collection('rooms')
          .doc(joinRoomID).get();

        if (!snapshot.exists) {
          Swal.fire(
            'ルームが存在しないようです...',
            '正しいルーム名を入力されているかご確認ください。',
            'warning'
          )
          return;
        }

        const mediaConnection = this.peer.joinRoom(joinRoomID, {
          mode: 'sfu',
          stream: this.localStream,
        });

        // participantsに自分を追加
        firebase.firestore().collection('rooms').doc(joinRoomID).collection('participants')
          .doc(this.peer.id).set({
            isMuted: true,
            isCameraOff: true,
            isReadyQuestion: false,
            joinedAt: firebase.firestore.FieldValue.serverTimestamp(),
          });

        this.roomID = joinRoomID;

        this.setEventListener(mediaConnection);
      },
      async addVideo(mediaConnection) {
        let remoteStreams = [];
        mediaConnection.on('stream', () => {
          // video要素にカメラ映像をセットして再生
          Object.keys(mediaConnection.remoteStreams).forEach( key => {
            const remoteStream = mediaConnection.remoteStreams[key];
            remoteStreams.push(remoteStream);
          });
        });
        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) {
        let remoteStreams = [];
        // video要素にカメラ映像をセットして再生
        Object.keys(mediaConnection.remoteStreams).forEach( key => {
          const remoteStream = mediaConnection.remoteStreams[key];
          remoteStreams.push(remoteStream);
        });
        await new Promise(resolve => setTimeout(resolve, 1000));
        remoteStreams.forEach(async remoteStream => {
          const videoElm = document.getElementById(remoteStream.peerId);
          videoElm.srcObject = remoteStream;
          videoElm.play();
        });
      },
      async setEventListener(mediaConnection) {
        this.addVideo(mediaConnection);
        const self = this;
        mediaConnection.on('peerJoin', () => {
          self.addVideo(mediaConnection);
        });
        mediaConnection.on('peerLeave', () => {
          self.removeVideo(mediaConnection);
        });
        $(window).on('unload', () => {
          mediaConnection.close();
          firebase.firestore().collection('rooms').doc(self.roomID)
            .collection('participants').doc(self.peer.id).delete();
        });

        await firebase.firestore().collection('rooms').doc(this.roomID).onSnapshot(snapshot => {
          const room = snapshot.data();
          this.isStarted = room.isStarted;
          this.right = room.right;
          this.isQuestion = room.isQuestion;
        });

        await firebase.firestore().collection('rooms').doc(this.roomID)
          .collection('participants').orderBy('joinedAt', 'asc').onSnapshot(snapshot => {
            const participants = [];
            snapshot.docs.map(doc => {
              if (doc.id === this.peer.id) return;
              const participant = doc.data();
              const id = doc.id;
              const isMuted = participant.isMuted;
              const isCameraOff = participant.isCameraOff;
              const isReadyQuestion = participant.isReadyQuestion;
              participants.push({id,isMuted,isCameraOff,isReadyQuestion});
            });
            this.participants = participants;
          });
      }
    },
  }
</script>
<style lang="scss">
  #my-id{
    display: none;
  }
  .p-dialogue {
    &__content{
      box-sizing: border-box;
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      flex-wrap: wrap;
      width: 70%;
      max-width: 100%;
      box-shadow: 3px 0 6px rgba(0,0,0,0.4);
      border-radius: 3px;
      padding: 35px;
      @media screen and (max-width: 1000px){
        width: 100%;
      }
    }
    &__txtWrap{
      width: 100%;
    }
    &__txt{
      font-size: 18px;
      display: block;
      text-align: right;
    }
    &__btnWrap{
      display: flex;
      justify-content: flex-end;
      width: 100%;
      margin-bottom: 30px;
    }
    &__btn{
      background: #fff;
      border: 1px solid #3c3c3c;
      border-radius: 3px;
      padding: 10px 30px;
      margin-right: 20px;
      // color: #fff;
      cursor: pointer;
      // font-weight: bold;
      &:hover{
        opacity: 0.7;
      }
    }
    &__videoWrap{
      position: relative;
      width: calc(50% - 15px);
      margin-bottom: 30px;
      box-shadow: 0 0 10px 10px rgba(249, 255, 100, 0.5);
      &--muted{
        box-shadow: 0 0 5px 5px rgba(0, 0, 0, 0.3);
      }
      video{
        vertical-align: bottom;
        width: 100%;
      }
    }
    &__mic,
    &__camera,
    &__timer {
      display: block;
      position: absolute;
      left: 5px;
      bottom: 5px;
      width: 40px;
      height: 40px;
      background: #fff;
      border-radius: 50%;
      padding: 10px;
      box-sizing: border-box;
      cursor: pointer;
    }
    &__mic {
      &--disabled {
        pointer-events: none;
      }
    }
    &__camera {
      left: 50px;
    }
    &__timer{
      text-align: center;
      left: auto;
      right: 10px;
    }
    &__person {
      display: none;
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      justify-content: center;
      align-items: center;
      background: #ddd;
      &--active{
        display: flex;
      }
      &__img{
        width: 30%;
      }
    }
  }
  .swal2-html-container{
    white-space: pre-wrap;
  }
</style>