src/administrative-sdk/speech-recording/speech-recording-controller.js
import Base64Utils from '../utils/base64-utils';
import Connection from '../connection/connection-controller';
import SpeechRecording from './speech-recording';
import when from 'when';
/**
* Controller class for the SpeechRecording model.
* @private
*/
export default class SpeechRecordingController {
/**
* @param {Connection} connection - Object to use for making a connection to the REST API and Websocket server.
*/
constructor(connection) {
this._connection = connection;
}
/**
* Initialise the speech recording challenge through RPCs.
*
* @param {SpeechChallenge} challenge - SpeechChallenge.
* @private
*/
speechRecordingInitChallenge(challenge) {
return this._connection.call('recording.init_challenge',
[this._connection._recordingId, challenge.id]).then(
// RPC success callback
recordingId => {
console.log('Challenge initialised for recordingId: ' + this._connection._recordingId);
return recordingId;
});
}
/**
* Initialise the speech recording audio specs through RPCs.
*
* @param {AudioRecorder} recorder - AudioRecorder.
* @param {Function} dataavailableCb - Callback.
* @private
*/
speechRecordingInitAudio(recorder, dataavailableCb) {
// Indicate to the socket server that we're about to start recording a
// challenge. This allows the socket server some time to fetch the metadata
// and reference audio to start the recording when audio is actually submitted.
const specs = recorder.getAudioSpecs();
return this._connection.call('recording.init_audio',
[this._connection._recordingId, specs.audioFormat], specs.audioParameters)
.then(recordingId => {
console.log('Accepted audio parameters for recordingId after init_audio: ' + this._connection._recordingId);
// Start listening for streaming data.
recorder.addEventListener('dataavailable', dataavailableCb);
return recordingId;
});
}
/**
* Start a speech recording from streaming audio.
*
* @param {SpeechChallenge} challenge - The speech challenge to perform.
* @param {AudioRecorder} recorder - The audio recorder to extract audio from.
* @returns {Promise.<SpeechRecording>} A {@link https://github.com/cujojs/when} Promise containing a {@link SpeechRecording}.
* @emits {string} 'ReadyToReceive' when the call is made to receive audio. The recorder can now send audio.
* @throws {Promise.<Error>} If challenge is not an object or not defined.
* @throws {Promise.<Error>} If challenge has no id.
* @throws {Promise.<Error>} If the connection is not open.
* @throws {Promise.<Error>} If the recorder is already recording.
* @throws {Promise.<Error>} If a session is already in progress.
* @throws {Promise.<Error>} If something went wrong during recording.
*/
startStreamingSpeechRecording(challenge, recorder) {
// Validate required domain model.
// Validate environment prerequisites.
if (typeof challenge !== 'object' || !challenge) {
return Promise.reject(new Error('"challenge" parameter is required or invalid'));
}
if (!challenge.id) {
return Promise.reject(new Error('challenge.id field is required'));
}
if (!this._connection._session) {
return Promise.reject(new Error('WebSocket connection was not open.'));
}
if (recorder.isRecording()) {
return Promise.reject(new Error('Recorder should not yet be recording.'));
}
if (this._connection._recordingId !== null) {
return Promise.reject(new Error('Session with recordingId ' + this._connection._recordingId +
' still in progress.'));
}
const self = this;
return new when.Promise((resolve, reject, notify) => {
self._connection._recordingId = null;
function _cb(data) {
const recording = new SpeechRecording(
challenge.id, data.userId, data.id, new Date(data.created), new Date(data.updated),
self._connection.addAccessToken(data.audioUrl));
resolve({recordingId: self._connection._recordingId, recording});
}
function recordedCb(activeRecordingId, audioBlob, forcedStop) {
self._connection.call('recording.close',
[self._connection._recordingId]).then(
// RPC success callback
res => {
// Pass along details to the success callback
_cb(res, forcedStop);
},
// RPC error callback
res => {
Connection.logRPCError(res);
reject(res);
});
recorder.removeEventListener('recorded', recordedCb);
recorder.removeEventListener('dataavailable', startStreaming);
}
// Start streaming the binary audio when the user instructs
// the audio recorder to start recording.
function startStreaming(chunk) {
const encoded = Base64Utils._arrayBufferToBase64(chunk);
console.log('Sending audio chunk to websocket for recordingId: ' +
self._connection._recordingId);
self._connection.call('recording.write',
[self._connection._recordingId, encoded, 'base64']).then(
// RPC success callback
res => {
// Wrote data.
console.log('Wrote data');
return res;
},
// RPC error callback
res => {
Connection.logRPCError(res);
reject(res);
}
);
}
function startRecording(recordingId) {
self._connection._recordingId = recordingId;
console.log('Got recordingId after initialisation: ' + self._connection._recordingId);
}
recorder.addEventListener('recorded', recordedCb);
self._connection.call('recording.init_recording', [])
.then(startRecording)
.then(() =>
self.speechRecordingInitChallenge(challenge)
.then(() => {
const p = new Promise(resolve_ => {
if (recorder.hasUserMediaApproval()) {
resolve_();
} else {
recorder.addEventListener('ready', resolve_);
}
});
p.then(() => {
self.speechRecordingInitAudio(recorder, startStreaming)
.catch(reject);
});
})
.then(() => notify('ReadyToReceive'))
)
.catch(reject);
})
.then(res => {
self._connection._recordingId = null;
return Promise.resolve(res);
})
.catch(error => {
self._connection._recordingId = null;
Connection.logRPCError(error);
return Promise.reject(error);
});
}
/**
* Get a speech recording in a speech challenge from the current active {@link Organisation} derived from the OAuth2
* scope.
*
* @param {string} challengeId - Specify a speech challenge identifier.
* @param {string} recordingId - Specify a speech recording identifier.
* @returns {Promise.<SpeechRecording>} Promise containing a SpeechRecording.
* @throws {Promise.<Error>} {@link SpeechChallenge#id} field is required.
* @throws {Promise.<Error>} {@link SpeechRecording#id} field is required.
* @throws {Promise.<Error>} If no result could not be found.
*/
getSpeechRecording(challengeId, recordingId) {
if (!challengeId) {
return Promise.reject(new Error('challengeId field is required'));
}
if (!recordingId) {
return Promise.reject(new Error('recordingId field is required'));
}
const url = this._connection._settings.apiUrl + '/challenges/speech/' + challengeId + '/recordings/' + recordingId;
return this._connection._secureAjaxGet(url)
.then(data => new SpeechRecording(challengeId, data.userId, data.id, new Date(data.created),
new Date(data.updated), this._connection.addAccessToken(data.audioUrl)));
}
/**
* Get and return all speech recordings in a specific speech challenge from the current active {@link Organisation}
* derived from the OAuth2 scope.
*
* @param {string} challengeId - Specify a speech challenge identifier to list speech recordings for.
* @returns {Promise.<SpeechRecording[]>} Promise containing an array of SpeechRecording.
* @throws {Promise.<Error>} {@link SpeechChallenge#id} is required.
* @throws {Promise.<Error>} If no result could not be found.
*/
getSpeechRecordings(challengeId) {
if (!challengeId) {
return Promise.reject(new Error('challengeId field is required'));
}
const url = this._connection._settings.apiUrl + '/challenges/speech/' + challengeId + '/recordings';
return this._connection._secureAjaxGet(url)
.then(data => data.map(datum => new SpeechRecording(challengeId, datum.userId, datum.id, new Date(datum.created),
new Date(datum.updated), this._connection.addAccessToken(datum.audioUrl))));
}
}