自己紹介
氏名:田原一樹
出身地:大阪
血液型:B型
所属:レアジョブ/App・UXチーム
フロントエンドエンジニア
本日のお話
弊社、レアジョブとは??
これらを目標・理念として掲げ、
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)
})
})
}
実用における弊社仕様ならではのツラミ
Media Pipeline Factoryを
使わせてもらっていることでの恩恵
まとめ
最後までご清聴頂き
ありがとうございました!