aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/AudioBox.tsx
diff options
context:
space:
mode:
authorbob <bcz@cs.brown.edu>2019-10-22 11:32:43 -0400
committerbob <bcz@cs.brown.edu>2019-10-22 11:32:43 -0400
commit1089aa451fd4571545e06b5674bc02ed1ecf4361 (patch)
treeba142bcd22ff100c75dd11c1ceeed0150cef310a /src/client/views/nodes/AudioBox.tsx
parente38afa49541fd0113f6f6b820c3bad769144f918 (diff)
added recording to audio documents and added to toolbar
Diffstat (limited to 'src/client/views/nodes/AudioBox.tsx')
-rw-r--r--src/client/views/nodes/AudioBox.tsx137
1 files changed, 125 insertions, 12 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 4c1c3a465..ee4e06a2e 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -2,37 +2,150 @@ import React = require("react");
import { FieldViewProps, FieldView } from './FieldView';
import { observer } from "mobx-react";
import "./AudioBox.scss";
-import { Cast } from "../../../new_fields/Types";
+import { Cast, DateCast } from "../../../new_fields/Types";
import { AudioField } from "../../../new_fields/URLField";
import { DocExtendableComponent } from "../DocComponent";
-import { makeInterface } from "../../../new_fields/Schema";
+import { makeInterface, createSchema } from "../../../new_fields/Schema";
import { documentSchema } from "../../../new_fields/documentSchemas";
+import { Utils } from "../../../Utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { runInAction, observable, reaction, IReactionDisposer, computed } from "mobx";
+import { DateField } from "../../../new_fields/DateField";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Doc } from "../../../new_fields/Doc";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { ContextMenu } from "../ContextMenu";
-type AudioDocument = makeInterface<[typeof documentSchema]>;
-const AudioDocument = makeInterface(documentSchema);
+interface Window {
+ MediaRecorder: MediaRecorder;
+}
+
+declare class MediaRecorder {
+ // whatever MediaRecorder has
+ constructor(e: any);
+}
+export const audioSchema = createSchema({
+ playOnSelect: "boolean"
+});
+
+type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>;
+const AudioDocument = makeInterface(documentSchema, audioSchema);
const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
@observer
export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
+ _reactionDisposer: IReactionDisposer | undefined;
+ @observable private _audioState = 0;
+
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
_ref = React.createRef<HTMLAudioElement>();
componentDidMount() {
- if (this._ref.current) this._ref.current.currentTime = 1;
+ runInAction(() => this._audioState = this.path ? 2 : 0);
+ this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
+ selected => {
+ let sel = selected.length ? selected[0].props.Document : undefined;
+ const extensionDoc = this.extensionDoc;
+ let start = extensionDoc && DateCast(extensionDoc.recordingStart);
+ let seek = sel && DateCast(sel.creationDate)
+ if (this._ref.current && start && seek) {
+ if (this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document)) {
+ let delta = (seek.date.getTime() - start.date.getTime()) / 1000;
+ if (start && seek && delta > 0 && delta < this._ref.current.duration) {
+ this._ref.current.currentTime = delta;
+ this._ref.current.play();
+ } else {
+ this._ref.current.pause();
+ }
+ } else {
+ this._ref.current.pause();
+ }
+ }
+ });
}
- render() {
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ }
+
+ _recorder: any;
+ recordAudioAnnotation = () => {
+ let gumStream: any;
+ let self = this;
+ const extensionDoc = this.extensionDoc;
+ extensionDoc && navigator.mediaDevices.getUserMedia({
+ audio: true
+ }).then(function (stream) {
+ gumStream = stream;
+ self._recorder = new MediaRecorder(stream);
+ extensionDoc.recordingStart = new DateField(new Date());
+ self._recorder.ondataavailable = async function (e: any) {
+ const formData = new FormData();
+ formData.append("file", e.data);
+ const res = await fetch(Utils.prepend(RouteStore.upload), {
+ method: 'POST',
+ body: formData
+ });
+ const files = await res.json();
+ const url = Utils.prepend(files[0].path);
+ // upload to server with known URL
+ self.props.Document[self.props.fieldKey] = new AudioField(url);
+ };
+ runInAction(() => self._audioState = 1);
+ self._recorder.start();
+ setTimeout(() => {
+ self._recorder.stop();
+ runInAction(() => self._audioState = 2);
+ gumStream.getAudioTracks()[0].stop();
+ }, 60 * 60 * 1000); // stop after an hour?
+ });
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ let funcs: ContextMenuProps[] = [];
+ funcs.push({ description: (this.Document.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.Document.playOnSelect = !this.Document.playOnSelect, icon: "expand-arrows-alt" });
+
+ ContextMenu.Instance.addItem({ description: "Audio Funcs...", subitems: funcs, icon: "asterisk" });
+ }
+
+ recordClick = (e: React.MouseEvent) => {
+ if (e.button === 0) {
+ if (this._recorder) {
+ this._recorder.stop();
+ runInAction(() => this._audioState = 2);
+ } else {
+ this.recordAudioAnnotation();
+ }
+ e.stopPropagation();
+ }
+ }
+
+ @computed get path() {
let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField);
let path = field.url.href;
+ return path === "https://actions.google.com/sounds/v1/alarms/beep_short.ogg" ? "" : path;
+ }
+
+ @computed get audio() {
+ let interactive = this.active() ? "-interactive" : "";
+ return <audio controls ref={this._ref} className={`audiobox-control${interactive}`}>
+ <source src={this.path} type="audio/mpeg" />
+ Not supported.
+ </audio>;
+ }
+
+ render() {
let interactive = this.active() ? "-interactive" : "";
- return (
- <div className="audiobox-container">
- <audio controls ref={this._ref} className={`audiobox-control${interactive}`}>
- <source src={path} type="audio/mpeg" />
- Not supported.
- </audio>
+ return (!this.extensionDoc ? (null) :
+ <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}>
+ {!this.path ?
+ <button className={`audiobox-record${interactive}`} style={{ backgroundColor: ["black", "red", "blue"][this._audioState] }}>
+ {this._audioState === 1 ? "STOP" : "RECORD"}
+ </button> :
+ this.audio
+ }
</div>
);
}