Skyway Media Pipeline Factoryを利用した
音声録音における実用例について

自己紹介

氏名:田原一樹

出身地:大阪

血液型:B型

所属:レアジョブ/App・UXチーム
フロントエンドエンジニア

  • Skyway Media Pipeline Factoryを利用した
    音声録音についての実用例についての紹介

本日のお話

弊社、レアジョブとは??

これらを目標・理念として掲げ、
Edtech企業として
活動しております。

コチラ

レッスンルーム

skypeからの脱却の為、
レッスンルームへ移行するにあたりWeb版では

使用しAppを構築しております。

様々なクラウドAPIを自由に組み合わせて、映像・音声をリアルタイムに処理できるプラットフォームです。
録音・録画、画像・音声認識、ライブ配信などのサービスを手軽に開発・運用できます。
端末からクラウドに映像・音声を送るときにSkyWayを利用します。

録音のアーキテクチャー

// 生徒・講師各アプリケーションで利用するdeviceを取得する。
navigator.mediaDevices.getUserMedia

// Peerオブジェクト作成
this.peerInstance = new Peer(this.peerId, options)

// open イベントでskywayシグナリングserverと接続
this.peerInstance.on('open', () => {…

// 生徒側からcallイベントを発火させ、講師側と接続
// 発信側
const mediaConnection = peer.call('peerID', mediaStream);

// 着信側
peer.on('call', mediaConnection => {
  // MediaStreamで応答する
  mediaConnection.answer(mediaStream);
});

// 各アプリケーション(講師側・生徒側)で相手のstreamObjectを取得する
mediaConnection.on('streamObject', stream => {
  // streamObjectをvuexのstoreに格納しておく。Mutationを呼び出してstateを更新
});
// Media Pipeline Factory用にもう一つPeerを生成する。(弊社の場合はこれを外部Classとしております。)
// class本体の抜粋

export default class ServerRecorder {
  static DEBUG_LEVEL = 3
  static METHOD = 'POST'
  static HEADERS = {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  }

constructor (
    streams: any[],
    recorderTimestamp: string,
    personId: string,
    personalName: string,
    memberId: string,
    recorderConfig: { SKYWAY_API_KEY: string, M_PIPE_ENTRY_POINT: string },
    appType: any,
    isRecordable?: boolean,
    tutorOrganization?: string
  ) {
    this.peerId = `${recorderTimestamp}_${personId}_${this.replaceName(personalName)}_${memberId}`
    this.recorderConfig = recorderConfig
    this.initialize()
  }
  private initialize (): void {
    try {
      this.recorderPeerInstance = new Peer(
        this.peerId,
        {
          key: this.recorderConfig.SKYWAY_API_KEY,
          debug: ServerRecorder.DEBUG_LEVEL
        }
      )
      this.mergeAudioTrack()
    }
    catch (error) {
      this.appType === 'LESSON'
        ? global.postLog({ operation: 'global/postActionLog', detail: `skyway-js SDK:${error}` }) // Lesson Room Postlog
        : global.postLog(`skyway-js SDK:${error}`) // TODO : Counseling Room Postlog
    }
  }
  private mergeAudioTrack (): void {
    // AudioContextをインスタンス化
    const audioContext = new AudioContext()
    // local/remoteのstreamが入っている配列をMediaStreamAudioSourceNodeのObject配列として生成。
    const sources = this.streams.map(stream => audioContext.createMediaStreamSource(stream))
    // 出力先(destination)のMediaStreamを生成
    const destination = audioContext.createMediaStreamDestination()
    // merge
    sources.forEach(source => source.connect(destination))
    this.mergeStream = destination.stream
  }
// アプリケーション内でのClassの呼び出し側

serverRecorder = new ServerRecorder(
  streamArray,
  Moment(this.lessonInfo.lessonStartTime).format('YYYYMMDDHHmm'),
  personId,
  personalName,
  memberId,
  {
  SKYWAY_API_KEY: skywayApiKey,
  M_PIPE_ENTRY_POINT: entryPoint
  },
  Constants.APP_TYPE,
  this.lessonInfo.isRecordable,
  this.lessonInfo.tutorOrganization
)
// アプリケーション側から発火される録音スタートのタイミングでMedia Pipeline Factory用に作成しておいたPeerのイベントを呼び出し。

public async start (): Promise<any> {
    try {
      this.recording = true
      // 録音準備処理
      await this.preparationRecorder()
      // streamインベントを実行するためのメソッド、録音成功時にresultStreamに返り値が渡る。
      this.resultStream = await this.createRecorderConnection()
      // skyway M-PIPE errorの際、イベントをキャッチ
      console.info('this.resultStream', this.resultStream)
      this.recorderPeerInstance.on('error', (error: any) => { throw error })
      if (this.resultStream) {
        return this.resultStream
      }
    }
    catch (error) {
      throw error
    }
  }
  
  private preparationRecorder ():Promise<any> {
    return new Promise((resolve, reject) => {
      // initialize内で生成したインスタンスでPeerOpenを行う。
      this.recorderPeerInstance.on('open', async (id:any) => {
        const params = {
          eventParams: {
            clientId: id
          }
        }
        const body = JSON.stringify(params)
        // M-PIPEでの録音を行うにあたり指定されたPOST/sessionについてのresをリクエスト
        const res = await fetch(
          `${this.recorderConfig.M_PIPE_ENTRY_POINT}/session`,
          {
            method: ServerRecorder.METHOD,
            headers: ServerRecorder.HEADERS,
            body })
          .catch(error => {
            // res自体がエラー時もstatusコードを返すのでコチラに入る可能性は無いが念の為、catch節を記載しreject
            const fetchErrorObject = {
              type: 'response fetch error',
              message: `${error}`
            }
            reject(fetchErrorObject)
          })
        // resのstatusコードが200番台かのチェック
        const isStatus = await this.errorHandle(res).catch(error => {
          const {
            status,
            statusText
          } = error

          const sessionErrorObject: object = {
            type: `skyway POST/session Error:${status}`,
            message: statusText
          }
          reject(sessionErrorObject)
        })

        if (!isStatus) { return }
        // callObjectの生成に必要なtokenとpeeridをget
        const { token, peerid } = await res.json().catch(error => {
          const errorObject: object = {
            type: 'Error: Can\'t get token or peerid',
            message: `${error}`
          }
          reject(errorObject)
        })
        // callObjectを生成
        this.recorderCallObj = this.recorderPeerInstance.call(peerid, this.mergeStream, { metadata: { token } })
        if (this.recorderCallObj) {
          resolve()
        }
      })
    })
  }
// 録音が実際にスタートする。

private async createRecorderConnection ():Promise<any> {
    // streamイベント(録音開始の処理)を一度だけ設定
    return new Promise((resolve, reject) => {
      this.recorderCallObj.once('stream', (stream:any) => {
        // ここに入った時点で、WebRTC セッションの確立が完了し、M-PIPEへの音声送信(録音)がスタート
        this.appType === 'LESSON'
          ? global.postLog({ operation: 'global/postActionLog', detail: `録音スタート:isRecordable:${this.isRecordable}:tutorOrganization:${this.tutorOrganization}` })
          : global.postLog('録音スタート')
        // streamイベントが成功しているので以下で設定しているerrorイベントは破棄する。
        this.recorderPeerInstance.removeListener('error', Promise.reject)
        resolve(stream)
      })
      // errorイベントを一度だけ発火するように設定。(録音成功時には破棄)
      this.recorderPeerInstance.once('error', (error: any) => {
        reject(error)
      })
    })
  }

実用における弊社仕様ならではのツラミ

  • 録音のアウトプット先が複数に渡る
    → M-PIPEでのEntryPointを複数準備し、アプリケーション内で振り分けを行う

     
  • 音声が双方向、または片側である場合がある
    → M-PIPEにわたすstreamオブジェクトの数を分岐させ、双方向の場合はマージさせている
    (マージの処理の実装が個人的に結構ハマりました…w)

Media Pipeline Factoryを
使わせてもらっていることでの恩恵

まとめ

  • skywayを利用したWebアプリケーションであれば、M-PIPEは組み込むことが可能
     
  • トライアル版ではあるが、やりたいことはアプリケーション内等で実装すれば実現は可能
     
  • 録音ができるとアプリケーションの幅が広がるので色んなことができるワクワクに繋がる。

最後までご清聴頂き
ありがとうございました!