diff --git a/packages/video/src/index.ts b/packages/video/src/index.ts index ff8b4c56..5d01c66e 100644 --- a/packages/video/src/index.ts +++ b/packages/video/src/index.ts @@ -1 +1,3 @@ -export default {}; +import TrialRecordExtension from "./trial"; + +export default { TrialRecordExtension }; diff --git a/packages/video/src/recorder.ts b/packages/video/src/recorder.ts new file mode 100644 index 00000000..d672d646 --- /dev/null +++ b/packages/video/src/recorder.ts @@ -0,0 +1,99 @@ +import autoBind from "auto-bind"; +import { JsPsych } from "jspsych"; + +/** + * + */ +export default class Recorder { + private _recorder?: MediaRecorder; + private blobs: Blob[] = []; + + /** + * + * @param jsPsych + */ + constructor(private jsPsych: JsPsych) { + autoBind(this); + } + + /** + * + */ + private get recorder() { + if (!this._recorder) { + this._recorder = this.jsPsych.pluginAPI.getCameraRecorder(); + this.recorder.addEventListener("dataavailable", this.handleDataAvailable); + this.recorder.addEventListener("stop", this.handleStop); + } + return this._recorder; + } + + /** + * + */ + public start() { + this.recorder.start(); + } + /** + * + */ + public stop() { + this.recorder.stop(); + this.recorder.stream.getTracks().map((t) => t.stop()); + } + + /** Handle recorder's stop event. */ + private async handleStop() { + await this.download(); + } + + /** + * Function ran at each time slice and when recorder has stopped. + * + * @param event - Event containing blob data. + */ + private handleDataAvailable(event: BlobEvent) { + this.blobs.push(event.data); + } + + /** Temp method to download data uri. */ + private async download() { + const data = (await this.bytesToBase64DataUrl( + new Blob(this.blobs), + )) as string; + const link = document.createElement("a"); + link.href = data; + link.download = `something_${new Date().getTime()} .webm`; + link.click(); + } + + /** + * Temp method to convert blobs to a data url. + * + * @param bytes - Bytes or blobs. + * @param type - Mimetype. + * @returns Result of reading data as url. + */ + private bytesToBase64DataUrl( + bytes: BlobPart, + type = "video/webm; codecs=vp8", + ) { + return new Promise((resolve, reject) => { + const reader = Object.assign(new FileReader(), { + /** + * When promise resolves, it'll return the result. + * + * @returns Result of reading data as url. + */ + onload: () => resolve(reader.result), + /** + * On error, promise return reader error. + * + * @returns Error message. + */ + onerror: () => reject(reader.error), + }); + reader.readAsDataURL(new File([bytes], "", { type })); + }); + } +} diff --git a/packages/video/src/trial.ts b/packages/video/src/trial.ts new file mode 100644 index 00000000..a091e36c --- /dev/null +++ b/packages/video/src/trial.ts @@ -0,0 +1,43 @@ +import autoBind from "auto-bind"; +import { JsPsych, JsPsychExtension, JsPsychExtensionInfo } from "jspsych"; +import Recorder from "./recorder"; + +/** This extension will allow reasearchers to record trials. */ +export default class TrialRecordExtension implements JsPsychExtension { + public static readonly info: JsPsychExtensionInfo = { + name: "chs-trial-record-extension", + }; + + private recorder: Recorder; + + /** + * Video recording extension. + * + * @param jsPsych - JsPsych object passed into extensions. + */ + public constructor(private jsPsych: JsPsych) { + this.recorder = new Recorder(this.jsPsych); + autoBind(this); + } + + /** Ran on the initialize step for extensions. */ + async initialize() {} + + /** Ran at the start of a trail. */ + public on_start() {} + + /** Ran when the trial has loaded. */ + public on_load() { + this.recorder.start(); + } + + /** + * Ran when trial has finished. + * + * @returns Trail data. + */ + public on_finish() { + this.recorder.stop(); + return {}; + } +}